JPA에서 MySQL 데이터베이스를 사용하고 saveAll
메서드를 호출하려 할 때 다음과 같은 쿼리가 날라갔습니다.
Hibernate:
insert
into
item_image
(image_url, item_id)
values
(?, ?)
Hibernate:
insert
into
item_image
(image_url, item_id)
values
(?, ?)
bulk insert 쿼리가 날라갈 것으로 예상했지만 INSERT
쿼리가 여러 번 날라가고 있었습니다.
이는 MySQL과 같은 데이터베이스에 기본키 생성을 위임하고 있었기 때문입니다.
saveAll
메서드를 들여다 보겠습니다.
@Transactional
@Override
public <S extends T> List<S> saveAll(Iterable<S> entities) {
Assert.notNull(entities, "Entities must not be null");
List<S> result = new ArrayList<>();
for (S entity : entities) {
result.add(save(entity)); // 문제 지점
}
return result;
}
@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);
}
}
public boolean isNew(T entity) {
ID id = getId(entity);
Class<ID> idType = getIdType();
if (!idType.isPrimitive()) {
return id == null;
}
if (id instanceof Number) {
return ((Number) id).longValue() == 0L;
}
throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
}
JPA의 영속성 컨텍스트에 엔티티가 존재하기 위해서는 식별자 값이 필요합니다.
그런데 IDENTITY
방식에서는 PK값 생성을 DB에 위임하기 때문에 persist
호출 시 엔티티를 영속성 컨텍스트에 등록하기 위해 INSERT
쿼리가 실행됩니다.
그렇다면 기본 키 생성전략이 IDENTITY
인 경우에는 bulk insert를 어떻게 수행할 수 있을까요?
제가 생각한 방법은 jdbcTemplate을 사용하는 방법이였습니다.
이를 위해서는 커스텀 레포지토리를 생성할 필요가 있었습니다. 이를 위해 JPA에서 제공하는 커스텀 레포지토리 기능을 이용했습니다.
public interface ItemImageRepositoryCustom {
void saveAllItemImages(List<ItemImage> itemImages);
}
위와 같이 bulk insert 연산을 수행할 메서드를 정의하고 있는 커스텀 레포지토리 인터페이스를 생성합니다.
이후 실제 구현을 담고 있는 클래스를 하나 생성합니다. 이때 주의해야할 점은 이름을 짓는 규칙이 존재한다는 것입니다.
레포지토리 인터페이스 이름 + Impl
로 네이밍을 해야 합니다.
이렇게 하면 Spring Data JPA가 사용자 정의 레포지토리로 인식하게 됩니다.
@RequiredArgsConstructor
public class ItemImageRepositoryImpl implements ItemImageRepositoryCustom {
private final NamedParameterJdbcTemplate jdbcTemplate;
@Override
public void saveAllItemImages(List<ItemImage> itemImages) {
// IDENTITY 방식의 한계로 bulk insert query 직접 구현
String sql = "INSERT INTO item_image "
+ "(image_url, item_id) VALUES (:imageUrl, :itemId)";
MapSqlParameterSource[] params = itemImages.stream()
.map(itemImage -> new MapSqlParameterSource()
.addValue("imageUrl", itemImage.getImageUrl())
.addValue("itemId", itemImage.getItem().getId()))
.collect(Collectors.toList())
.toArray(MapSqlParameterSource[]::new);
jdbcTemplate.batchUpdate(sql, params);
}
}
마지막으로 레포지토리 인터페이스에서 사용자 정의 인터페이스를 상속받으면 됩니다.
public interface ItemImageRepository extends JpaRepository<ItemImage, Long>, ItemImageRepositoryCustom {
}
JPA를 사용하면서 saveAll 메서드를 사용하면서 bulk insert 쿼리가 날라갈 것으로 기대했지만 INSERT 쿼리가 여러번 날라갔습니다.
이는 JPA에서 IDENTITY 방식을 사용하면서 생기는 한계였습니다.
이를 해결하기 위해 저는 JdbcTemplate을 직접 사용하게 되었습니다.