[JPA] MySQL에서의 saveAll

JeongYong Park·2023년 9월 14일
1

개요

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을 직접 사용하게 되었습니다.

profile
다음 단계를 고민하려고 노력하는 사람입니다

0개의 댓글