Spring Data JPA를 사용하면서 알지 못했던 내용들을 정리합니다.
아래 내용에 대한 코드는 Github에서 확인할 수 있습니다.
ParentService.save() =
@Transactional
ChildService.save() =@Transactional(propagation = REQUIRES_NEW)
시나리오
ex. try-catch 문을 사용하여 childService.save()
메서드에 대한 예외 처리를 해줘야 한다.
@Transactional(readOnly = true) 옵션을 주고, save를 호출한다면?
SimpleJpaRepository - save
@Transaction
어노테이션이 별도로 설정되어있는 것을 볼 수 있음.@Transactional(readOnly = true)
옵션을 주더라도, save()
메서드는 @Transactional
옵션을 따르게 됨.N+1이 @OneToOne 관계에서도 발생한다?
@OneToOne
관계의 Default FetchType은 EAGER@OneToOne
관계에서는 문제가 없지만, 양방향 @OneToOne 관계에서는 무조건 EAGER로 동작한다.정확히 얘기하자면
OneToOne 양방향 연관 관계에서 연관 관계 주인이 아닌 엔티티를 조회할 때, LAZY 전략이 무시되고 EAGER 전략으로 동작한다.
Proxy
를 만들어 줘야 한다.아래는 테이블의 구조
@OneToOne
양방향 관계인 상태link : YOUTUBE - NHN Cloud
Entity Mapping
연관관계 매핑
관계형 데이터베이스에서는 Join을 통해 연관된 테이블을 참조하는 반면,
Java에서는 객체 참조를 이용하여 연관된 엔터티를 참조하게 된다.
다중성(Multiplicity)
방향성
영속성 전이(persistence cascade)
N:1 연관관계 매핑
[Hibernate]
/* insert for
com.f1v3.jpa.domain.Player */insert
into
player (created_at, name, player_id)
values
(?, ?, default)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player, type, player_detail_id)
values
(?, ?, ?, default)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player, type, player_detail_id)
values
(?, ?, ?, default)
정상적으로 Player Entity에 대한 데이터 저장 후
PlayerDetail Entity에 대한 저장이 이루어짐.
1:N 연관관계 매핑
[Hibernate]
/* insert for
com.f1v3.jpa.domain.Player */insert
into
player (created_at, name, player_id)
values
(?, ?, default)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player_id, type)
values
(?, ?, ?)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player_id, type)
values
(?, ?, ?)
[Hibernate]
update
player_detail
set
player_id=?
where
player_id=?
and type=?
[Hibernate]
update
player_detail
set
player_id=?
where
player_id=?
and type=?
로그를 살펴보면, Player ID에 대한 정보를 추가적으로 저장하기 위해, Update 쿼리가 발생하는 것을 확인할 수 있습니다.
따라서 이러한 경우, 1:N 단방향 보다는 양방향으로 설정하여 이러한 문제를 방지하는 것이 좋다.
1:N -> 양방향 매핑 (@MapsId)를 한 경우
[Hibernate]
/* insert for
com.f1v3.jpa.domain.Player */insert
into
player (created_at, name, player_id)
values
(?, ?, default)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player_player_id, type)
values
(?, ?, ?)
[Hibernate]
/* insert for
com.f1v3.jpa.domain.PlayerDetail */insert
into
player_detail (description, player_player_id, type)
values
(?, ?, ?)
단일 레코드
에 대해서만 적용Pagination을 사용하기 위해서는 Fetch JOIN을 사용하는 쿼리와 분리해서 사용하는 것이 좋은 방향
해결방법 : List를 Set으로 변경, @OrderColumn
JPA Repository 메서드로는 JOIN 쿼리를 실행할 수 없다?
public interface PlayerRepository extends JpaRepository<Player, Long> {
// SELECT * FROM Players WHERE name = {name};
List<Player> findByName(String name);
// SELECT * FROM Players WHERE name = {name} AND created_at > {created_at};
List<Player> findByNameAndCreateDateAfter(String name, LocalDateTime createDate);
}
// SELECT * FROM Player p
// INNER JOIN PlayerDetail pd
// ON p.player_id = pd.player_id
// WHERE pd.type = {type};
List<Player> findByDetails_Pk_Type(String type);
위와 같은 방식으로 JOIN 쿼리를 실행할 수 있다.
Page<Player> getAllByName(String name, Pageable pageable);
Slice<Player> readAllByName(String name, Pageable pageable);
Page는 Slice를 상속받으며 getTotalPages(), getTotalElements() 등 페이지에 관련된 메서드가 존재!
추론을 해보자면, '개수'에 대한 쿼리가 한 번 더 나가지 않을까라고 생각을 했는데,
실제로 수행되는 쿼리를 봐보자.
// SELECT * FROM Players WHERE name = {name} offset {offset} limit {limit};
// SELECT COUNT(*) FROM Players WHERE name = {name}
Page<Player> getAllByName(String name, Pageable pageable);
// SELECT * FROM Players WHERE name = {name} offset {offset} limit {limit};
Slice<Player> readAllByName(String name, Pageable pageable);