A db에서 데이터를 가져와 B db에 넣는 배치 작업을 수행할 때 두가지 문제 발생
문제1. insert 하기 전에 select가 실행됨
문제2. repository.saveAll(List) 을 통해 다량의 데이터를 넣는데, 멀티 insert가 되지 않고 행별로 insert가 됨
(십만 개를 넣으면 10만번 select, insert 가 수행되는 상황)
select 되는 원인 분석
여러 fk의 조합으로 pk가 되어 있는데, 이 경우 이미 key값이 정해진 것으로 판단하여 insert할 때 update를 해야하는지 확인하는 로직이 존재. 그래서 아래 구문에서 isNew()가 false가 되어 merge가 수행되면서 발생한다.
@Transactional
@Override
public <S extends T> S save(S entity) {
Assert.notNull(entity, "Entity must not be null.");
if (entityInformation.isNew(entity)) {
em.persist(entity);
return entity;
} else {
return em.merge(entity);
}
}
해결 >>
entity에 Persistable<>를 implemets 하고 isNew()를 오버라이드 한다. 필요에 따라 getId()도 오버라이드 한다.
참조 문서에서는 다른 케이스로 insert 되는 것까지 고려하여 isNew가 false가 되야하는 상황도 고려되어 있으니 필요시 참조. 내 경우에는 해당 table에 insert는 일일 배치만 되기 때문에 필요가 없어서 적용하지 않았다.
@Entity
@Table(name = "table name")
public class ReservedSeat implements Persistable<String> {
.
.
.
.
@Override
public boolean isNew() {
return true;
}
}
멀티 insert가 되지 않는 현상에 대한 해결법 리스트
1
properties에 몇가지 옵션을 추가한다.
jpa.properties.hibernate.order_inserts: true
jpa.properties.hibernate.order_updates: true
jpa.properties.hibernate.jdbc.batch_size: 10000
spring.datasource.hikari.data-source-properties.rewriteBatchStatements: true
를 추가하라고 한다.
적용 후 >> 안됨. 실패.
2.
postgresql 은 옵션명이 rewriteBatchedInserts 라고 한다. jdbc url에 추가
적용 후 >> 안됨. 실패
3.
GenerationType.IDENTITY 옵션을 사용하면 내부적으로 batch insert를 사용할 수 없다고 함
>> 사용하지 않는 중
4.
Spring에서 제공하는 jdbcTemplate으로 sql을 날린다.
적용 후 >> 적용됨. 성능 향상됨
다른 방법이 있는지는 모르겠지만, 구글에는 mysql에 대한 정보만 있고 postgresql에 대한 설정값이 명확하게 나와있는 것은 찾을 수 없다. jdbcTemplate은 정상 적용 확인되었으니 해당 방법을 사용한다.
적용 코드
batchUpdate()는 여러 개가 있고, 각각 파라미터가 다르지만 아래 함수가 가장 나아보인다.
아래처럼 만들고 entityJdbcRepository.saveAll(List)를 호출하면 된다.
그리고 JPA와 병행해서 배치를 수행할 경우, @Transactional 을 꼭 붙여서 실패 케이스에 대응할 수 있도록 하자.
@Repository
@AllArgsConstructor
public class EntityJdbcRepository {
private final JdbcTemplate jdbcTemplate;
@Transactional
public void saveAll(List<Entity> datas) {
jdbcTemplate.batchUpdate(
"INSERT INTO table (`id`, `name`) VALUES (?, ?)",
datas, // insert할 데이터 리스트
10000, // 1회에 진행할 배치 사이즈(10만개 데이터라면 만 개 씩 10번 돌아감)
(ps, i) -> {
ps.setString(1, subItems.get(i).getId());
ps.setString(2, subItems.get(i).getName());
}
}
}
최종 적용 건.
@Repository
@AllArgsConstructor
public class PurCndJdbcRepository {
private final JdbcTemplate jdbcTemplate;
/**
* 배치 insert
*
* @param purCnds PurCnd 콜렉션 객체
* @return batchSize로 나눠서 insert 수행한 횟수
*/
@Transactional
public int batchSaveAll(List<PurCnd> purCnds) {
return batchSaveAll(purCnds, Constants.DEFAULT_BATCH_SIZE);
}
@Transactional
public int batchSaveAll(List<PurCnd> purCnds, int batchSize) {
return jdbcTemplate.batchUpdate(
"insert into pur_cnd(pur_cnd_cd, pur_cnd_nm, sup_cd, sales_chnl_cd, "
+ "frst_reg_dtm, frst_reg_usr_id, frst_reg_pgm_nm, last_mod_dtm, "
+ "last_mod_usr_id, last_mod_pgm_nm, vld_pur_cnd_yn) "
+
"values(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
purCnds,
batchSize,
(ps, argument) -> {
ps.setString(1, argument.getPurCndCd());
ps.setString(2, argument.getPurCndNm());
ps.setString(3, argument.getSupCd());
ps.setString(4, argument.getSalesChnlCd());
ps.setString(5, DateUtil.now());
ps.setString(6, ThreadContext.getCurrentUsrId());
ps.setString(7, ThreadContext.getCurrentScrId());
ps.setString(8, DateUtil.now());
ps.setString(9, ThreadContext.getCurrentUsrId());
ps.setString(10, ThreadContext.getCurrentScrId());
ps.setString(11, argument.getVldPurCndYn());
}).length;
}
}
참조
https://taesan94.tistory.com/266
https://velog.io/@qotndus43/Batch-Insert
댓글