@Entity
public class User{
...
@OneToMany(fetch = FetchType.EAGER)
@ToString.Exclude
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private List<UserHistory> userHistories = new ArrayList<>();
@OneToMany
@JoinColumn(name = "user_id")
@ToString.Exclude
private List<Review> reviews = new ArrayList<>(); // null point exception 방지
}
user를 호출할때마다 userHistory가 항상 필요하진 않을것(않은 경우가 더 많음)
-> userHistory는 실제로 필요한 시점에만 쿼리를 실행하면 될 것
-> 필요한 시점을 JPA에서 아는 방법은 Getter에 의한 호출
즉 유저 객체를 가져오기 위해서 쿼리가 실행되고 난 후 만약 유저 객체에 대한 히스토리가 필요해서 userRepository.getHistories()
하는 시점에 쿼리가 실행되어 유저 히스토리 리스트를 가져오는 것 -> LAZY FETCH
반면 User 객체를 호출하면 관련 릴레이션을 모두 조회해오는 것을 -> EAGER FETCH
LAZY는 세션(영속성 컨텍스트가 엔티티를 관리하고 있는 시점
)이 열려 잇을 때만 가능
-> @Transactional
을 통해 트랜잭션이 열려있을 때 그 주기안에서만 LAZY FETCH가 동작
could not initialize proxy - no Session
-> 이 경우 @Transactional을 붙이거나 EAGER FETCH를 통해 해결
public class Review extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
private float score;
@ManyToOne
private User user;
@ManyToOne
private Book book;
@OneToMany
@JoinColumn(name = "review_id")
@ToString.Exclude
private List<Comment> comments;
}
또한 ToString
을 하게 되면 지정된 위에 보이듯 여러 속성값을 출력하기 위해 GETTER
를 사용하게 됨
ToString, JSON Serialize(REST API 결과가 보통 JSON)를 쓸 때 Exclude하지 않고 getter를 쓰면 LAZY 패치의 장점을 잃음
-> 엔티티 실행시마다 관련 릴레이션 쿼리를 실행하기에
연관관계의 FetchType default 값
EAGGER
LAZY
Review.java
@ManyToOne
@ToString.Exclude
private Book book;
이 경우 리뷰 엔티티를 가져올때 book 정보를 항상 가져옴
@ManyToOne(fetch = FetchType.LAZY) 사용 시 쿼리가 간단해짐
하지만 EAGER,LAZY는 쿼리하는 시점에 문제이지 N+1 즉 쿼리 횟수
에 영향을 끼치진 않음
Review
...
public class Review extends BaseEntity{
...
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@ToString.Exclude
private Book book;
@OneToMany(fetch = FetchType.EAGER)
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
테스트 코드
EAGER의 경우 조회 쿼리가 실행되면 영속성 컨텍스트 캐쉬를 통해 데이터 조회 (쿼리 추가 실행 X)
@Transactional
void reviewTest() {
List<Review> reviews = reviewRepository.findAll();
// System.out.println(reviews);
System.out.println("전체를 가져왔습니다.");
System.out.println(reviews.get(0).getComments());
System.out.println("첫번째 리뷰의 코멘트들을 가져왔습니다.");
System.out.println(reviews.get(1).getComments());
System.out.println("두번째 리뷰의 코멘트들을 가져왔습니다.");
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
전체를 가져왔습니다.
[]
첫번째 리뷰의 코멘트들을 가져왔습니다.
[]
두번째 리뷰의 코멘트들을 가져왔습니다.
}
즉 조회쿼리를 getComments를 한다고 해서 쿼리를 추가적으로 실행하는 것이 아니라 앞서 실행한 FindAll을 바탕으로 영속성 컨텍스트에 있는 캐시에 값을 가져와 프린트
...
public class Review extends BaseEntity{
...
@OneToMany(fetch = FetchType.LAZY)
@JoinColumn(name = "review_id")
private List<Comment> comments;
}
Hibernate:
select
review0_.id as id1_6_,
review0_.created_at as created_2_6_,
review0_.updated_at as updated_3_6_,
review0_.book_id as book_id7_6_,
review0_.content as content4_6_,
review0_.score as score5_6_,
review0_.title as title6_6_,
review0_.user_id as user_id8_6_
from
review review0_
전체를 가져왔습니다.
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
첫번째 리뷰의 코멘트들을 가져왔습니다.
Hibernate:
select
comments0_.review_id as review_i5_4_0_,
comments0_.id as id1_4_0_,
comments0_.id as id1_4_1_,
comments0_.created_at as created_2_4_1_,
comments0_.updated_at as updated_3_4_1_,
comments0_.comment as comment4_4_1_,
comments0_.review_id as review_i5_4_1_
from
comment comments0_
where
comments0_.review_id=?
[]
두번째 리뷰의 코멘트들을 가져왔습니다.
필요한 순간, 즉 조회 메서드를 실행시킬때 조회 쿼리가 생성되어 실행됨
EAGER, LAZY는 쿼리 실행시점의 문제, N+1의 실행 문제는 여전히 존재
@Query를 통해 Fetch 쿼리를 Custom
ReviewRepository.java
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
@Query(value = "select distinct r from Review r join fetch r.comments") // 속성값 밑 엔티티 이름 사용
List<Review> findAllByFetchJoin();
}
data.sql
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values(1, '내 인생을 바꾼책', '너무너무 좋았어요', 5.0, 1, 1);
insert into review(`id`, `title`, `content`, `score`, `user_id`, `book_id`) values(2, '진도가 빨라요', '조금 별로였어요', 3.0, 2, 2);
insert into comment(`id`, `comment`, `review_id`) values(1, '저도 좋았어요', 1);
insert into comment(`id`, `comment`, `review_id`) values(2, '저는 별로 였습니다.', 1);
insert into comment(`id`, `comment`, `review_id`) values(3, '저는 그냥 그랬습니다.', 2);
List<Review> reviews = reviewRepository.findAllByFetchJoin();
reviews.forEach(System.out::println);
Hibernate:
select
review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
inner join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2022-11-24T14:50:44.225102, updatedAt=2022-11-24T14:50:44.225102), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2022-11-24T14:50:44.225959, updatedAt=2022-11-24T14:50:44.225959), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-11-24T14:50:44.226472, updatedAt=2022-11-24T14:50:44.226472), id=2, comment=저는 별로 였습니다.)])
Review(super=BaseEntity(createdAt=2022-11-24T14:50:44.225574, updatedAt=2022-11-24T14:50:44.225574), id=2, title=진도가 빨라요, content=조금 별로였어요, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2022-11-24T14:50:44.226775, updatedAt=2022-11-24T14:50:44.226775), id=3, comment=저는 그냥 그랬습니다.)])
distinct 안붙이면 cross join에 의해 리뷰가 2개지만 3개 프린트되었음
쿼리가 한번 수행된것 볼 수 있다 -> N+1 문제 해결
@EntityGraph 사용
(2.1 버전 이후부터 사용가능)fetch join과 같은 효과, 쿼리 메서드에도 적용가능(ex: findAll()
)
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
...
//findAllByEntityGraph(), findAll() 모두 동일한 결과
@EntityGraph(attributePaths = "comments")
@Query("select r from Review r")
List<Review> findAllByEntityGraph();
or
@EntityGraph(attributePaths = "comments")
List<Review> findAll();
}
@Test
@Transactional
void reviewTest() {
//findAllByEntityGraph(), findAll() 모두 동일한 결과
List<Review> reviews = reviewRepository.findAllByEntityGraph();
or
List<Review> reviews = reviewRepository.findAll();
reviews.forEach(System.out::println);
}
//실행결과
Hibernate:
select
review0_.id as id1_6_0_,
comments1_.id as id1_4_1_,
review0_.created_at as created_2_6_0_,
review0_.updated_at as updated_3_6_0_,
review0_.book_id as book_id7_6_0_,
review0_.content as content4_6_0_,
review0_.score as score5_6_0_,
review0_.title as title6_6_0_,
review0_.user_id as user_id8_6_0_,
comments1_.created_at as created_2_4_1_,
comments1_.updated_at as updated_3_4_1_,
comments1_.comment as comment4_4_1_,
comments1_.review_id as review_i5_4_1_,
comments1_.review_id as review_i5_4_0__,
comments1_.id as id1_4_0__
from
review review0_
left outer join
comment comments1_
on review0_.id=comments1_.review_id
Review(super=BaseEntity(createdAt=2022-11-24T14:57:24.666739, updatedAt=2022-11-24T14:57:24.666739), id=1, title=내 인생을 바꾼책, content=너무너무 좋았어요, score=5.0, comments=[Comment(super=BaseEntity(createdAt=2022-11-24T14:57:24.667415, updatedAt=2022-11-24T14:57:24.667415), id=1, comment=저도 좋았어요), Comment(super=BaseEntity(createdAt=2022-11-24T14:57:24.667926, updatedAt=2022-11-24T14:57:24.667926), id=2, comment=저는 별로 였습니다.)])
Review(super=BaseEntity(createdAt=2022-11-24T14:57:24.667055, updatedAt=2022-11-24T14:57:24.667055), id=2, title=진도가 빨라요, content=조금 별로였어요, score=3.0, comments=[Comment(super=BaseEntity(createdAt=2022-11-24T14:57:24.668222, updatedAt=2022-11-24T14:57:24.668222), id=3, comment=저는 그냥 그랬습니다.)])
N+1이라고 무조건 잘못 된것이 아님
파일 크기가 크거나, 작은 파일이여도 쿼리를 많이 발생 시 오히려 시스템 과부하를 시킬수 있음
-> 어떤 방식이 더 적합한지 판단 후 적용하는 것이 중요