프로젝트 진행 중 대량의 insert를 수행할 필요가 있었습니다.
시나리오는 아래와 같습니다.
1. ticket 이 만료되면 ticket_record 가 생성됩니다. ticket_record는 만료시간, 활성시간(단위: 초) 필드를 가집니다.
2. 하루가 종료되는 시점에 만료되지 않은 ticket 이 존재하면 일괄적으로 만료시켜야 합니다.
3. ticket 의 일괄 만료에 따라 ticket_record 의 일괄 생성이 필요합니다.
JPA saveAll()
@Test
void saveAll() {
//given
List<Member> members = new ArrayList<>();
for(int i=0; i<10; i++) {
Member member = new Member("member" + i);
members.add(member);
}
//when
memberRepository.saveAll(members);
}
JPA의 saveAll() 메서드의 경우 원소 개수만큼 쿼리가 생성되어 batch insert에는 적절하지 않았습니다.
Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.
하이버네이트 공식문서 12.Batching 항목에 따르면 identity 식별자 생성기를 사용하는 경우 JDBC 수준에서 insert batcing을 비활성화한다고 합니다.
제가 진행중인 프로젝트에서는 식별자 생성 전략으로 identity를, DB로 MySQL을 사용하고 있었습니다. sequence 전략을 이용한다면 batch insert를 수행할 수 있다고 하나, 테이블 전략을 바꾸는 것보다 기본 테이블 전략을 유지한채로 batch insert를 수행하고자 다른 방법을 선택하였습니다.
jdbcTemplate.batchUpdate()
Spring Data JPA 의존성에는 JDBC 역시 포함되어 있습니다.
JDBC는 JdbcTemplate의 batchUpdate() 메소드를 통해 하나의 쿼리로 대량의 update를 지원합니다.
@RequiredArgsConstructor
public class MemberRepositoryImpl implements CustomMemberRepository{
private final JdbcTemplate template;
@Override
public void saveMembers(List<String> names) {
String sql = "insert into member (name) values (?)";
template.batchUpdate(sql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
String name = names.get(i);
ps.setString(1, name);
}
@Override
public int getBatchSize() {
return names.size();
}
});
}
}
batchUpdate의 첫번째 인자로는 실행할 sql을 전달합니다.
두번째 인자로는 BatchPreparedStatementSetter 인터페이스의 익명 구현 객체를 생성해서 전달해줍니다.
setValues 메소드에서는 주어진 질의문에 대해 파라미터 바인딩을 통해 값을 바인딩시켜줍니다.
테스트
@Test
void saveMembers() {
//given
List<String> names = new ArrayList<>();
for(int i=0; i<10; i++) {
names.add("member" + i);
}
//when
memberRepository.saveMembers(names);
//then
List<Member> members = memberRepository.findAll();
assertThat(members.size()).isEqualTo(10);
}
이대로 테스트를 진행할 경우 기존 JPA의 saveAll() 메소드와 동일하게 원소의 갯수만큼 쿼리가 나가게 됩니다.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?rewriteBatchedStatements=true&profileSQL=true&logger=Slf4JLogger&maxQuerySizeToLog=999999
username: root
password: root123!
application.yml에 spring.datasource.url에 rewriteBatchedStatements=true 를 추가해줍니다.
- profileSQL=true: Driver에서 전송하는 쿼리를 출력합니다.
- logger=Slf4JLogger: Driver에서 쿼리 출력시 사용할 로거를 설정합니다.
- maxQuerySizeToLog=999999 : 출력할 쿼리 길이를 지정합니다.
자세한 설정에 관해서는 아래 링크에서 찾아볼 수 있습니다.
rewriteBatchedStatements의 설명에 따르면 해당 설정이 SQL injection을 허용할 수 있으니 주의하라는 언급이 있습니다.
그리고 다시 테스트 코드를 실행시키면 아래와 같이 한 번의 쿼리로 batch insert가 수행된 것을 확인할 수 있었습니다.
참고
'JPA' 카테고리의 다른 글
일대다 페이지네이션 최적화하기 (0) | 2023.12.13 |
---|---|
엔티티 매니저와 영속성 컨텍스트 (0) | 2023.07.30 |
[querydsl] in절 동적 쿼리 작성 주의점(1=2) (0) | 2023.04.03 |
JPA에서 네이티브 SQL 사용하기 (0) | 2023.03.06 |