본문 바로가기
Java/Spring

[Spring data JPA] 조회: Entity + 연관 Entity(LAZY) + Pageable

by java개발자 2021. 4. 24.

기준: springboot 2.4.4

1:N (Team: Member)

 

*주의!! @EntityGraph가 뭔가 새로운 것인줄 알았는데, 결국 내부 로직은 fetch이다.

 

엔티티, 연관엔티티 모두 한번에 조회를 하고 싶고, Page기능까지 추가하고 싶다.

(뷰를 위한 rest api에서는 page기능은 필수이다. 그리고 대부분 엔티티 단위로 조회를 할 것이다.)

 

시도1.

inner join fetch + Pageable

에러..

query specified join fetching, but the owner of the fetched association was not present in the select list................

안되는줄 알았지만 그냥 해봤다...

 

시도2.

@EntityGraph를 이용하면 연관된 엔티티도 같이 가져온다.

N:1 에서는 잘됨.

1:N 에서는 limit sql이 만들어지지 않음. (일단 DB에서 다 가져와서, app에서 페이징 처리한다는데... row가 많아지면... 말도안되는 소리...)

==> 어찌됐든 결과는 만들어지므로, 메모리 이슈가 눈으로는 보이지 않는다. 주의주의!!!!)

 

시도3.

LAZY -> EAGER로 바꾼다고 한들... N+1 문제는 해결되지 않는다.

 

시도4

N+1을 1+1로 바꾼다.

==> LAZY로 지연로딩을 한다. 단 지연로딩시 발생하는 N번의 조회를 1번으로 바꿀수 있다.

1. 메인 쿼리 실행 (TEAM)

SELECT
	t.*
FROM
    TEAM t
limit ?,?

2. 연관 엔티티를 IN으로 한번에 실행해서 1차캐시에 저장

SELECT
    m.*
FROM Member m
WHERE m.id IN (t.member_id,,,,,,,,,,,,)

3. N번의 조회를 할 수도 있지만, 그 N번이 이미 1차 캐시에 들어가 있으므로 DB조회 불필요.

==> 과연 그럴까? 1번 TEAM에 연관된 Member의 프록시를 벗기면, 1차 캐시에서 가져오지 않고, 새로운 SQL이 실행된다... 왜??? (Member는 다 준비가 되었는데, 뭐가 문제일까? Member만 단독으로 조회해서??)

 

2번에서 반대로 조회했던 Member 내의 Team은 프록시가 아니므로, app에서 Member에서 Team을 꺼내서 distinct하고, 재정렬해주면 될 듯... 귀찮네..

 

일단 정리

1.메인 쿼리(+페이징)
PageRequest pageRequest2 = PageRequest.of(0, 4, Direction.DESC, "name");
Page<Team> find = teamRepository.findByName12(pageRequest2);

// where가 붙을 수 있어서 일단, JPQL로...
@Query("select t from Team t")
public Page<Team> findByName12(Pageable pageable);


2.연관엔티티를 IN으로 조회해서 1차 캐시에 쌓는다.
List<Team> teams = find.getContent();
List<Long> teamIds = teams.stream().mapToLong((t) -> t.getId()).boxed().collect(Collectors.toList());
List<Member> members = memberRepository.findByTeamIdIn(teamIds);

// 메소드만으로도 가능하지만 query를 직접작성해야 inner join이 된다.
@Query("select m from Member m where m.team.id in :teamId")
public List<Member> findByTeamIdIn(@Param("teamId") List<Long> teamId);

===> 1차 캐시가 업데이트가 되면, 그 전에 생성된 프록시객체를 1차 캐시의 엔티티로 바꿔주는 뭐,, 그런건 없나?

===> 1차 캐시가 제대로 업데이트가 되었는지부터 확인이 필요할 듯.

// 직접 Member를 꺼내보니 SQL없이 조회가 된다. 이미 1차 캐시에 있다는 것..
Member member120 = memberRepository.findById(120L).orElse(null);

그럼 다시... 질문.

프록시를 초기화할 때, 이미 1차 캐시에 있지만 왜 DB를 조회할까?

아 설마.. DB 조회는 하지만 1차 캐시에 있으므로 버리고, 1차 캐시를 리턴하나?

JPQL에 해당하는 이야기이고, LAZY 로딩은 1차 캐시에서 가져오는게 맞는데;;;

컬렉션내의 lazy 로딩은 로직이 다른가? 재조회 한 SQL의 extracted value가 일부인것을 보니... 일단 SQL 조회를 하고 나서, 1차 캐시에 있으므로 1차 캐시를 반환하고 있다.

==> 정리하면, 지연로딩 때문에 프록시 객체를 생성하는 시점에, 1차 캐시에 있으면 실객체를 반환하지만, 만약 없다면 계속 프록시로 남게 되고, 프록시를 실제 사용할때 초기화가 되고, 초기화는 DB조회를 시도한다. DB조회 시도해서 1차 캐시에 있다면, 1차 캐시의 데이터를 반환, 없으면 DB조회 결과를 반환.

문제는... 초기화 할때는 왜 1차 캐시를 찾아보지 않는가???

    select
        members0_.team_id as team_id13_2_0_,
        members0_.id as id1_2_0_,
        members0_.id as id1_2_1_,
        members0_.reg_date as reg_date2_2_1_,
        members0_.update_date as update_d3_2_1_,
        members0_.city as city4_2_1_,
        members0_.state as state5_2_1_,
        members0_.street as street6_2_1_,
        members0_.plus_four as plus_fou7_2_1_,
        members0_.zip as zip8_2_1_,
        members0_.name as name9_2_1_,
        members0_.area_code as area_co10_2_1_,
        members0_.local_number as local_n11_2_1_,
        members0_.provider_id as provide12_2_1_,
        members0_.team_id as team_id13_2_1_ 
    from
        member members0_ 
    where
        members0_.team_id=?
2021-04-25 14:07:54.780 TRACE 26424 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1118]
2021-04-25 14:07:54.780 TRACE 26424 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_1_] : [BIGINT]) - [120]
2021-04-25 14:07:54.780 TRACE 26424 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([team_id13_2_0_] : [BIGINT]) - [1118]
2021-04-25 14:07:54.780 TRACE 26424 --- [nio-8080-exec-1] o.h.type.descriptor.sql.BasicExtractor   : extracted value ([id1_2_0_] : [BIGINT]) - [120]

이러면,,, 방법이 없다.

정말 app으로

Team에 있는 Member 프록시는 버리고,

Member 실객체를 Team에 다시 연결시켜주는 작업 필요

 

 

==========================================================================

시도5

1+1

1. 페이지기능으로 Team을 검색하고 teamIds만 리턴,

2. 검색된 teamIds로 재검색(페이지기능 없이, @EntityGraph로)

 

==> 이 기능이 그냥 깔끔하닷.

1.메인 쿼리(+페이징)
PageRequest pageRequest2 = PageRequest.of(0, 4, Direction.DESC, "name");
Page<Long> find = teamRepository.findByName13(pageRequest2);

// where가 붙을 수 있어서 일단, JPQL로...
@Query("select t.id from Team t")
public Page<Long> findByName13(Pageable pageable);

2.페이징 없이 ids로 재조회(이번엔 EntityGraph로.. 또는 그냥 inner join fetch 해도 된다.)
List<Long> teamIds = find.getContent();
List<Team> find2 = teamRepository.findByIdIn(teamIds);

@EntityGraph(attributePaths = {"members"}, type = EntityGraph.EntityGraphType.LOAD)
// @Query("select t from Team t inner join fetch t.members where t.id in :teamId")
public List<Team> findByIdIn(@Param("teamId") List<Long> teamId);

그리고, Team에 Member뿐만 아니라, 다른 1:N (Member2)이 또 있는 경우 @EntityGraph로 Member와 Member2를 함께 가져올 수 있다.

아!!! 에러.. 동시에 1:N을 두번은 못하는구나...;; @EntityGraph 도 결국 fetch join 이다!!

org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [org.example.models.Team.members, org.example.models.Team.members2]

 

시도6

1:N Team: Member

1:N Team: Member2 일때는 어떻게 하나?

 

@BatchSize
1:N 관계가 2개 이상인 경우
데이터가 많은쪽에 join fetch
데이터가 적은쪽에 BatchSize (EAGER라면 함께 바로 조회, LAZY라면 사용할때 조회)

 

==> pageable과 함께 사용할려면,

결국 1이 되는 메인 Entity 조회(+pageable)를 하고,

1:N은 @BatchSize (1:N의 fetch는 안됨.)

1:N은 @BatchSize (1:N의 fetch는 안됨.)

N:1은 fetch join 또는 @EntityGraph 

N:1은 fetch join 또는 @EntityGraph 

 

 

======================================================

정리.

일반 엔티티의 프록시객체는 getName 처럼 사용할 때, 초기화됨.

컬렉션의 프록시객체인 PersistentBag는 리스트를 꺼낼때get(i) 특정 요소만 초기화됨.

질문.

Team 리스트 조회 ==> Team 리스트는 1차 캐시에 저장

teamIds로 Member 리스트 조회 ==> Member 리스트는 1차 캐시에 저장

(LAZY로딩)Team의 Members에서 get(i): 특정 Member를 꺼내면 DB에서 재조회. 왜?(case1)

만약, Member에 @BatchSize가 걸려있다면 IN으로 size만큼 Members를 재조회해서, 특정 Member 뿐만 아니라, 다른 Member들도 초기화를 같이 시켜줌.

 

초기화를 같이 시켜주는 로직이 궁금하다.

조회한 DB결과(영속성 엔티티)를 이용해서 초기화를 시켜주는 작업을, 내가 직접 할 수 있다면, (case1)을 해결할 수 있을듯.

'Java > Spring' 카테고리의 다른 글

[security] 정리  (0) 2021.05.06
[security] getPassword() is null  (0) 2021.05.06
[Spring data JPA] native query + XML + DTO  (0) 2021.04.24
JPA 씹어먹기  (0) 2021.04.19
springBoot2 jpa Test Errors  (1) 2021.04.05