JPA Bulk Insert
안녕하세요! 이번 포스팅에서는 JPA에서 Insert를 할 경우 여러 데이터들을 어떻게 한번의 네트워크 통신으로 Insert하는지 알아보겠습니다.
사전 준비
- JPA 프로젝트
- MySQL
그리고, 디렉토리와 코드들은 다음과 같습니다.
applicatoin.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/test
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
Api.java
@RestController
@RequiredArgsConstructor
public class Api {
private final ArticleService articleService;
private final CommentService commentService;
@PostMapping("/articles")
public String article(ArticleDto articleDto) {
articleService.post(articleDto);
return "ok";
}
@PostMapping("/articles/{articleId}/comments")
public String comments(@PathVariable Long articleId,
CommentDtoList commentDtoList) {
commentService.post(articleId, commentDtoList);
return "ok";
}
}
Dto들
// ArticleDto
@Getter @Setter
public class ArticleDto {
private String title;
}
// CommentDtoList
@Getter @Setter
public class CommentDtoList {
private List<CommentDto> comments;
@Getter @Setter
public static class CommentDto {
private String comment;
}
}
Api.java
@RestController
@RequiredArgsConstructor
public class Api {
private final ArticleService articleService;
private final CommentService commentService;
@PostMapping("/articles")
public String article(ArticleDto articleDto) {
articleService.post(articleDto);
return "ok";
}
@PostMapping("/articles/{articleId}/comments")
public String comments(@PathVariable Long articleId,
CommentDtoList commentDtoList) {
commentService.post(articleId, commentDtoList);
return "ok";
}
}
Article.java
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Article {
@Id @GeneratedValue(strategy = IDENTITY) @Column(name = "ARTICLE_ID")
private Long id;
private String title;
public static Article of(ArticleDto articleDto) {
return Article.builder()
.title(articleDto.getTitle())
.build();
}
}
Comment.java
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Comment {
@Id @GeneratedValue(strategy = IDENTITY) @Column(name = "COMMENT_ID")
private Long id;
@ManyToOne(fetch = LAZY)
@JoinColumn(name = "ARTICLE_ID")
private Article article;
private String comment;
public static List<Comment> of(Article article, List<CommentDtoList.CommentDto> comments) {
return comments.stream()
.map(commentDto -> Comment.of(article, commentDto))
.collect(toList());
}
private static Comment of(Article article, CommentDtoList.CommentDto commentDto) {
return Comment.builder()
.article(article)
.comment(commentDto.getComment())
.build();
}
}
ArticleService.java
@Service
@RequiredArgsConstructor
@Transactional
public class ArticleService {
private final ArticleRepository articleRepository;
public void post(ArticleDto articleDto) {
articleRepository.save(Article.of(articleDto));
}
}
ArticleRepository.java
public interface ArticleRepository extends JpaRepository<Article, Long> {
}
CommentService.java
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {
private final ArticleRepository articleRepository;
private final CommentRepository commentRepository;
public void post(Long articleId, CommentDtoList commentDtoList) {
Article article = articleRepository.findById(articleId).orElseThrow();
List<Comment> commentList = Comment.of(article, commentDtoList.getComments());
commentRepository.saveAll(commentList);
}
}
CommentRepository.java
public interface CommentRepository extends JpaRepository<Comment, Long> {
}
이제 기초적인 프로젝트 구성은 다 끝났습니다.
순수 JPA에서 다량의 데이터를 한번에 삽입할 경우
먼저, 하나의 Article을 넣고, 여러 Comment 들을 넣어보겠습니다.
http://localhost:8080/articles
{
"title" : "제목"
}
http://localhost:8080/articles/1/comments
{
"comments" : [
{
"comment" : "1번 댓글"
},
{
"comment" : "2번 댓글"
}
]
}
그 다음, 로그를 살펴보면
이렇게 Comment 테이블에 두 개의 Insert가 나가는 것을 확인할 수 있습니다.
rewriteBatchedStatements=true
이제 한번의 네트워크 통신으로 여러 Comment 데이터들을 삽입해보겠습니다. 우선 application.yml의 datasource 부분을 다음과 같이 수정합니다.
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
MySQL 공식문서 사이트에 가보면 이 옵션에 대한 설명이 나와있습니다.
이 옵션을 적용하고 다시 article과 comment를 넣어보겠습니다.
똑같이 Comment의 갯수만큼 insert가 발생합니다.. 그 원인은 엔티티에 존재합니다. 엔티티들을 다시 살펴보면 @GeneratedValue(strategy = IDENTITY)
가 id 필드 위에 표기되어있습니다.
이 어노테이션의 역할은 Primary Key를 자동생성 해주는 전략을 지정합니다. 이 프로젝트에서 선택한 IDENTITY의 의미는 Primary Key 값 지정을 DB에 위임하겠다는 의미입니다. 객체에 데이터를 넣게되면 id값은 null인 상태로 save를 할텐데, 다른 전략과는 다르게 IDENTITY는 id값이 null인 상태로 DB에 직접 전달하고, DB가 알아서 Primary Key를 셋팅해줍니다.
이 IDENTITY 전략이 bath insert를 지원하지 않는 이유는 여기 stackoverflow 에서 보다 자세한 내용을 참조하실 수 있습니다.
물론 IDENTITY 전략 말고도 SEQUENCE
와 TABLE
전략도 있습니다. 하지만 현재 쓰는 DB인 MySQL은 시퀀스를 지원하지 않고, 테이블 전략을 쓰기에는 이 테이블도 관리해야 한다는 점이 부담이었습니다.
그렇다면 어떻게 해결해야할까요?
JdbcTemplate
방금 말했듯, SEQUENCE
와 TABLE
전략을 쓰지 못한다면 jdbcTemplate을 쓰는 방법이 존재합니다.
CommentJdbcRepository.java
@Repository
@RequiredArgsConstructor
public class CommentJdbcRepository {
private final JdbcTemplate jdbcTemplate;
public void saveAll(Article article, List<Comment> comments) {
Long articleId = article.getId();
jdbcTemplate.batchUpdate("insert into comment(article_id, comment) " +
"values(?, ?)",
new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
ps.setLong(1, articleId);
ps.setString(2, comments.get(i).getComment());
}
@Override
public int getBatchSize() {
return comments.size();
}
});
}
}
수정된 CommentService.java
@Service
@RequiredArgsConstructor
@Transactional
public class CommentService {
private final ArticleRepository articleRepository;
private final CommentJdbcRepository commentJdbcRepository;
public void post(Long articleId, CommentDtoList commentDtoList) {
Article article = articleRepository.findById(articleId).orElseThrow();
List<Comment> commentList = Comment.of(article, commentDtoList.getComments());
commentJdbcRepository.saveAll(article, commentList);
}
}
이제 실제로 bulk insert가 되는지 로그로 확인해야 합니다. 저는 4건의 comment를 넣었는데, 인텔리제이 로그에서 확인할 수 없어 mysql의 로그를 확인해야 합니다.
데이터는 정상적으로 insert 되었고,
로그를 확인했을 때도 하나의 insert into 문에 데이터들이 insert 되는 것을 확인할 수 있습니다.
소마에서 프로젝트를 진행하다 보니 은근히 한번의 api 호출에 같은 테이블의 여러 데이터들을 저장해야하는 경우가 빈번히 있었습니다. 이 부분을 JPA 환경에서는 어떻게 해결할지에 대해 좋은 학습이었다고 생각하며, 실제로 한번의 명령에 규모가 엄청 큰 데이터들을 한번에 insert 할 때는 batch size를 설정하여 batch size 만큼 insert하는 방법도 좋은 방법이라고 생각합니다.
그럼 오늘 포스팅은 여기서 마무리하겠습니다!
참고한 자료들