on
NHN Cloud Spring JPA의 사실과 오해 요약
NHN Cloud Spring JPA의 사실과 오해 요약
반응형
단방향 vs 양방향
사실상 단방향 매핑만으로 연관관계 매핑은 이미 완료
단방향 매핑에 비해 양방향 매핑은 복잡하고 객체에서 양쪽 방향을 모두 관리해줘야 함
양방향 매핑은 단방향 매핑에 비해 반대 방향으로의 객체 그래프 탐색 기능이 추가된 것 뿐
대개의 경우 단방향 매핑이면 충분하다
우선은 단방향 매핑을 사용하고 반대 방향으로의 객체 그래프 탐색이 필요할 때 양방향을 사용
다대일
@Entity public class Member { @Id @Column(name = "member_id") private Long memberId; private String name; @Column(name = "create_dt") private LocalDateTime createDate; }
@Entity public class MemberDetail { @Id @Column(name = "member_detail_id") private Long memberDetailId; @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "member_id") private Member member; private String type; private String description; }
@Test void create(){ Member member = new Member("member1", LocalDateTime.now()); MemberDetail memberDetail1 = new MemberDetail(); memberDetail1.setMember(member); memberDetail1.setType("type1"); memberDetail1.setDescription("member1-type1"); MemberDetail memberDetail2 = new MemberDetail(); memberDetail2.setMember(member); memberDetail2.setType("type2"); memberDetail2.setDescription("member1-type2"); //cascade.all로 인해 member 엔티티도 같이 저장됨 memberDetailRepository.saveAll(Arrays.asList(memberDetail1, memberDetail2)); }
member insert query
memberdetail insert query
memberdetail insert query
총 3개의 쿼리 / 원하는 대로 들어감
일대다
@Entity @Data public class Member { @Id @Column(name = "member_id") private Long memberId; private String name; @Column(name = "create_dt") private LocalDateTime createDate; @OneToMany(cascade = CascadeType.ALL) @JoinColumn(name = "member_id") private List details; }
@Entity @Data public class MemberDetail { @EmbeddedId private Pk pk; private String description; @Embeddable public static class Pk implements Serializable{ @Column(name = "member_id") private Long memberId; private String type; } }
@Test void create(){ Member member = new Member("member1", LocalDateTime.now()); Member savedMember = memberRepository.save(member); MemberDetail memberDetail1 = new MemberDetail(); memberDetail1.setPk(new MemberDetail.Pk(savedMember.getMemberId(), "type1")); memberDetail1.setDescription("member1-type1"); MemberDetail memberDetail2 = new MemberDetail(); memberDetail2.setPk(new MemberDetail.Pk(savedMember.getMemberId(), "type1")); memberDetail2.setDescription("member1-type2"); member.getDetails().add(memberDetail1); member.getDetails().add(memberDetail2); }
기대하는 것
Member insert 1번
MemberDetail insert 2번
실제 쿼리
Member insert 1번
MemberDetail insert 2번
MemberDetail update 2번
오해 - 양방향 매핑보다 단방향 매핑이 좋다
일대다(1:N) 단방향 연관관계 매핑에서 영속성 전이를 통한 insert 시
일대다(1:N) 관계의 외래 키(FK) 지정을 위해 추가적인 update 쿼리가 발생하는 문제
이 경우에는 오히려 일대다(1:N) 양방향 연관관계로 변경하면 추가적인 update 쿼리가 없어짐
양방향
@Entity @Data public class Member { @Id @Column(name = "member_id") private Long memberId; private String name; @Column(name = "create_dt") private LocalDateTime createDate; @OneToMany(cascade = CascadeType.ALL, mappedBy = "member") private List details; }
@Entity @Data public class MemberDetail { @EmbeddedId private Pk pk; private String description; @ManyToOne @MapsId("memberId") //기본키를 외래키로 쓰는경우 @MapsId 사용, 아니면 @JOinColumn 사용 private Member member; @Embeddable public static class Pk implements Serializable{ @Column(name = "member_id") private Long memberId; private String type; } }
*-ToOne (@OneToOne, @ManyToOne) : FetchType.EAGER
*-ToMay (@OneToMany, @ManyToMany) : FetchType.LAZY
N + 1문제
하나의 쿼리로 N개의 레코드를 가져왔을 때,
연관관계 Entity를 가져오기 위해 쿼리를 N번 추가적으로 수행하는 문제
해결방법
Fetch Join
Entity Graph
N + 1 문제에 대해 흔히들 하는 오해
N + 1 문제는 EAGER Fetch 전략때문에 발생한다?
> LAZY로 설정했더라도 연관 Entity를 참조하면 그 순간 추가적인 쿼리가 수행
findAll() 메서드는 N + 1 문제를 발생시키지 않는다?
Fetch 전략을 적용해서 연관 Entity를 가져오는 것은 오직 단일 레코드에 대해서만 적용
단일 레코드 조회가 아닌 경우 (JPQL을 수행하는 경우, findAll() 메서드 역시 이 경우)
> 해당 JPQL을 먼저 수행(Entity에 설정된 Fetch 전략 적용 안됨
> 반환된 레코드 하나 하나에 대해 Enitty에 설정된 Fetch 전략을 적용해서 연관 Entity 가져옴
> 그렇기 때문에 findAll() 메서드 호출도 역시 이 과정에서 N + 1 문제 발생 가능
Fetch JOIN으로 N + 1 문제 해결 시 흔히 하는 실수
Pagination + Fetch JOIN
매우 위험
Pagination 쿼리에 Fetch JOIN을 적용하면 실제로는 모든 레코드를 가져오는 쿼리가 실행된다.
> limit 조건없이 쿼리를 모두 가져온다음 메모리에서 limit이 적용되는 구조
> Pagination 쿼리와 Fetch JOIN 쿼리를 분리해서 사용해야함
둘 이상의 컬렉션을 Fetch JOIN - MultipleBagFetchException
Java의 java.util.List 타입은 기본적으로 HIbernate의 Bag 타입으로 맵핑됨
Bag은 Hibernate에서 중복 요소를 허용하는 비순차 컬렉션
둘 이상의 컬렉션(Bag)을 Fetch Join하는 경우
> 그 결과로 만들어지는 카테시안 곱에서 어느 행이 유효한 중복을 포함하고 있고
> 어느 행이 그렇지 않은 지 판단할 수 없어 MultipleBagFetchException 발생
해결방법 - List를 Set으로 변경, @OrderColumn
MultipleBagFetchException에 대한 추가 내용
https://jojoldu.tistory.com/457
오해 - JPA Repositorty 메서드와 JOIN
JPA Repository 메서드로는 JOIN 쿼리를 실행할 수 없다?! > 되는데요
HInts!!
> JPA Repository에서는 "_"를 통해 내부 속성값을 접근할 수 있다.
@Entity public class Person { @Id private Long id; @Embedded private Address address; @Embeddable public static class Address { String zipcode, city, street; } }
public interface PersonRepository extends JpaRepository { List findByAddress_Zipcode(String zipcode); }
방법
@Entity @Data public class Member { @Id @Column(name = "member_id") private Long memberId; @OneToMany private List details; }
@Entity @Data public class MemberDetail { @EmbeddedId private Pk pk; @Embeddable public static class Pk implements Serializable{ @Column(name = "member_id") private Long memberId; private String type; } }
public interface MemberRepository extends JpaRepository { // select * from Member m // inner join MemberDetail md // on m.member_id = md.member_id // where md.type = {type} List findByDetails_Pk_Type(String type) }
Page getAllByName(String name, Pageable pageable); Slice readAllByName(String name, Pageable pageable);
차이점
//select * from Members where name = {name} offset {offset} limit {limit} //select count(*) from Members where name = {name} Page getAllByName(String name, Pageable pageable); //select * from Members where name = {name} offset {offset} limit {limit_plus_1} Slice readAllByName(String name, Pageable pageable;)
Slice는 count 쿼리가 실행되지 않는다.
Slice는 전달한 limit보다 레코드를 하나더 가져와서, 레코드가 있는지 없는지 보고 next가 있는지 없는지 판단
데이터가 많은 테이블에서 count(*) 쿼리가 부담스럽다면 Slice 사용
Page를 쓸때도 페이징을 할 여지가 만족되야 (최소 2페이지 이상) count(*) 쿼리가 나감
오해 - JPA Repository 메서드와 DTO Prjjection
JPA Repository 메서드로는 DTO Projection을 할 수 없다?!
되는데요
DTO Projection
Class 기반 (DTO) Projection
Interface 기반 Prjoection
Dynamic Prjoection
Class 기반 (DTO) Projection
//final로 선언된 필드에 대해서 생성자를 제공 @Value public class MemberDto { private final String name; private final LocalDateTime localDateTime; }
Collection findByName(String name);
Interface 기반 Prjoection
@Entity @Data public class Member { @Id @Column(name = "member_id") private Long memberId; private String name; }
// projection interface public interface MemberNameOnly { String getName(); }
// select name from Members // where create_dt > {createDate} Collection findByCreateDateAfter(LocalDateTime createdDate);
인터페이스만 만들면 구현객체는 Spring Data JPA가 프록시로 만들어줌
Interface 기반 Prjoection2
interface 중첩 구조 지원
@Value + SpeL(target 변수)
@Entity @Data public class Member { @Id @Column(name = "member_id") private Long memberId; private String name; @OneToMany private List details; }
@Entity @Data public class MemberDetail { @EmbeddedId private Pk pk; private String description; @Embeddable public static class Pk implements Serializable{ @Column(name = "member_id") private Long memberId; private String type; } }
public interface MemberDto { String getName(); List getDetails(); interface MemberDetailDto{ @Value("#{target.pk.type}") String getType(); String getDescription(); } }
MemberDetailDto의 target은 MemberDetail 엔티티와 매핑
static class인 Pk안의 type에 매핑
Dynamic Projection
Collection findByCreateDateAfter(LocalDateTime createDate, Class type);
Collection nameOnlies = memberRepository.findByCreateDateAfter( LocalDateTime.now(), MemberNameOnly.class); Collection memebrDtos = memberRepository.findByCreateDateAfter( LocalDateTime.now(), MemberDto.class);
Spring Data Jpa의 Projection에 대한 추가 내용
https://catsbi.oopy.io/bdb22ab6-4e58-4f45-a234-90f08da38b08
참고
https://www.youtube.com/watch?v=rYj8PLIE6-k
반응형
from http://dncjf64.tistory.com/358 by ccl(A) rewrite - 2021-12-11 19:27:05