Spring Data JPA

youngkyu MIn·2023년 12월 27일

movie

양방향보단 단방향?

영속성 전이를 통한 Insert 시

다대일 단방향

memberDetail(Many) -> member(One)

의도대로 member, memberdetail1, memberdetail2 -> insert 3번


일대다 단방향

MemberDetail(One) -> member(Many)

member, memberdetail1, memberdetail2 -> insert 3번 후
-> member update 2회


객체 그래프 탐색 측면에서는 단방향이 양방향보다 좋을 수 있으나

영속성 전이를 통해 insert 수행을 할 때는 그렇지 않다.


N + 1

@OneToOne, @ManyToOne => 기본 FetchType.EAGER

@OneToMany, @ManyToMany => 기본 FetchType.Lazy


Fetch 전략이 LAZY 로 설정했더라도 연관 Entity를 참조하면 그 순간 추가적인
쿼리가 발생한다.

  • EAGER 든 LAZY 든 객체 참조가 이뤄지면 추가 쿼리가 발생한다.

findAll() 에서도 단일 레코드 조회가 아닌 경우 ( JPQL 을 수행하는 경우)

  • 해당 JPQL 을 먼저 수행 (Entity 에 설정된 Fetch 전략 적용 안 됨)

  • 반환된 레코드 하나 하나에 대해 Entity에 설정된 Fetch 전략을 적용해서 연관 Entity 가져옴

  • 때문에 findAll() 메서드 호출도 역시 N + 1 문제 발생 가능


Pagination 과 Fetch JOIN

Pagination 쿼리에 Fetch JOIN 을 적용하면 실제로는 모든 레코드를 가져오는 쿼리가 실행된다.

  • 다 긁어오고 메모리에서 limit 를 계산함.


Fetch Join - MultipleBagFetchException

  • 자바에서 List 타입은 Hibernate 의 Bag 타입으로 맵핑
  • Bag 은 중복 요소를 허용하는 비순차 컬렉션임
  • 둘 이상의 컬렉션(Bag) 을 Fetch Join 하는 경우
    • 그 결과로 만들어지는 카테시안 곱 (Cartesian Product) 에서
      어느 행이 유효한 중복을 포함하고 있는 판단할 수 없어 exception 발생
  • List를 Set 으로 바꾸거나 @OrderColumn 사용

JPA RePository 메서드로 JOIN 쿼리 실행

@Entity
public class Member {
	@OneToMany
    private List<MemberDetail> details;
}

@Entity
public class MemberDetail {
	@EmbeddedId
    private Pk pk;
    
    @Embeddable
    public static class PK {
    	private String type;
    }
}

public interface MemberRepository extends JpaRepository<Member, Long> {

	// select * from Member m 
    // inner join MemberDetail md
    // on m.member_id = md.member_id
    // where md.type = {type}
    
   	List<Member> findByDetails_Pk_Type(String Type);

Page vs Slice

Page

  • limit 대로 레코드 가져오는 쿼리 하나
  • count 쿼리도 하나
  • 쿼리 두번

Slice

  • limit 대로 레코드 가져올 때 +1 개 더 가져옴 ( 이를 통해 다음 페이지 존재를 파악 )
  • 쿼리 한번

JPA Repository 메서드로 DTO Projection

Class 기반 ( DTO ) Projection

@Value
public class MemberDto {
	private final String name;
    private final LocalDateTime createDate;
}

public interface Member Repository extends JpaRepository<Member, Long> {
	Collection<MemberDto> findByName(String name);
}

Interface 기반 Projection

@Entity
@Table(name = "Members")
public class Member {
	private String name;
}

//projection interface
public interface MemberNameOnly {
	String getName();
}

public interface MemberRepository extends JpaRepository<Member, Long> {
	Collection<NMemberNameOnly> findByCreateDateAfter(LocalDateTime createDate);
}

Interface 기반 Projection

  • Interface 중첩 구조 지원
  • @Value + SpEL ( target 변수 )
@Entity  
public class Member {
	private String name;
    @OneToMany
    private List<MemberDetail> details;
}

@Entty
public class MemberDetial {
	@EmbeddedId
    private Pk pk;
    
    private String description;
    
    @Embeddable
    public static class Pk {
    	private String type;
   }
}
 
public interface MemberDto {
	String getName();
    List<MemberDetailDto> getDetails();
    
    interface MemberDetailDto {
    	@Value("#{target.pk.type}") // MemberDetail Entity 가 target 에 매핑
        String getType();
        
        String getDescription();
   }
}

Dynamic Projection

public interface MemberRepository extends JpaRepository<Member, Long> {
	<T> Collection<T> findByCreateDateAfter(LocalDateTime createDate, Class<T> type);
}

Collection<MemberNameOnly> nameOnlies 
	= memberRepository.findByCreateDateAfter(LocalDateTime.now(), MemberNameOnly.class);
    
Collection<MemberDto> memberDtos
	= memberRepository.findByCreateDateAfter(LocalDateTime.now(), MemberDto.class);

인자로 넘기는 class 타입대로 projection 발생하여 반환

정리

연관관계 매핑

  • 사실상 단방향 매핑만으로 연관관계 매핑은 이미 완료
  • 대개의 경우 단방향 매핑이면 충분
  • 일대다 단방향 연관관계 매핑에서 영속성 전이를 사용할 경우 양방향으로 변경하는게 유리

Spring Data JPA Repository

  • JpaRepository 상속 - 웬만한 CRUD, Paging, Sorting 메서드 사용
  • 메서드 이름 규칙을 통한 쿼리 생성 - 이름 규칙에 따라 interface에 메서드 선어만 하면 쿼리 생성
  • JPA Repository 메서드로도 JOIN 쿼리 수행 가능 - 이름 규칙에 따라 Entity 내 연관관계 필드 탐색
  • JPA Repository 메서드에서도 다양한 DTO Projection 지원
profile
한 줄 소개

0개의 댓글