JPA ) 다대다 관계 목록 조회 fetch & paging
다음과 같이 동호회(Club)와 회원(Member) 엔티티가 있다.
// 동호회
@Entity
@Getter
@Setter
public class Club {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "club_id")
private Long id;
private String name;
@OneToMany(mappedBy = "club", fetch = FetchType.LAZY)
private List<ClubMembers> members = new ArrayList<>();
}
// 회원
@Entity
@Getter
@Setter
pubic class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;
private String name;
@OneToMany(mappedBy = "member", fetch = FetchType.LAZY)
private List<ClubMembers> clubs = new ArrayList<>();
}
@Entity
@Getter
@Setter
public class ClubMembers {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = Fetch.LAZY)
private Club club;
@ManyToOne(fetch = Fetch.LAZY)
private Member member;
}
회원은 여러 동호회에 가입할 수 있고, 동호회도 여러 회원을 받을 수 있다.
클럽 목록 조회 무한스크롤 페이지를 위하여 다음과 같이 JPQL를 작성한다면,
@Query("SELECT c FROM Club c"
+ " JOIN FETCH c.members cm"
+ " JOIN FETCH cm.member m"
+ " WHERE c.id < :lastId"
+ " AND c.name like %:keyword%")
List<Club> findFetchWithPaging(@Param("lastId") Long lastId, @Param("keyword") String keyword, Pageable pageable);
원하는 결과는 얻을 수 있지만,
언젠가는 "HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!" 라는 경고를 발견하게 될 것이다.
이는 생성된 쿼리를 보면 알 수 있다.
select
club0_.club_id as club1_23_0_,
members1_.id as club1_24_1_,
members2_.member_id as member_1_18_2_,
club0_.name as name11_23_0_,
members1_.member_id as member_6_24_1_,
members1_.club_id as open_api7_24_0__,
members1_.id as open_api1_24_0__,
member2_.name as name_14_18_2_
from
club club0_
inner join
club_members members1_
on club0_.club_id=members1_.club_id
inner join
member member2_
on members1_.member_id=member2_.member_id
where
club0_.club_id < ?
and (
club0_.name like concat('%', ?, '%')
)
order by
club0_.club_id desc
쿼리를 보면 페이징을 위한 limit 쿼리가 없다.
그렇다면 어떻게 원하는 결과 수만큼 가져오는 것일까?
생각해 보면 동호회에 회원 정보를 fetch 하기 위해서는 동호회에 소속된 멤버만큼 쿼리 결과 Row 수가 늘어날 것이기 때문에 limit 을 사용하면 원하는 만큼 데이터를 불러올 수 없게 된다.
이 때문에 Hibernate 에서는 조건에 맞는 모든 결과를 불러와서 메모리에 올려둔 후에 이를 정리하여 사용자가 원하는 갯수만큼의 객체를 반환한다.
따라서 많은 메모리를 사용하게 될 수 있고, 실사용 중 OOM(Out Of Memory)이 발생할 수 있기 때문에 위와 같이 경고가 나오는 것이다.
이를 미리 깨닫고 방지하기 위해서 메모리에서 페이징 처리하는 것을 사전에 차단해 주는 옵션을 사용하면 좋다.
spring.jpa.properties.hibernate.query.fail_on_pagination_over_collection_fetch=true
출처 : https://jojoldu.tistory.com/737
그렇다면 어떻게 해야 원하는 만큼의 객체와 데이터를 받아올 수 있을까?
"HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!"를 구글링하여 가장 먼저 나오는 stackoverflow 결과를 참고하면
두 개의 쿼리로 나누어 결과를 도출할 수 있다.
먼저 조건의 맞는 동호회의 id만을 가져오고, 해당 id에 fetch하여 데이터를 불러오는 것이다.
@Query("SELECT c.id FROM Club c"
+ " WHERE c.id < :lastId"
+ " AND c.name like %:keyword%")
List<Long> findIdsWithPaging(@Param("lastId") Long lastId, @Param("keyword") String keyword, Pageable pageable);
@Query("SELECT distinct(c) FROM Club c"
+ " JOIN FETCH c.members cm"
+ " JOIN FETCH cm.member m"
+ " WHERE c.id IN :ids")
List<Club> findFetchByIds(@Param("ids") List<Long> ids, Sort sort);
참고로 distinct가 생략되면 객체가 중복되어 결과를 준다.
// 중복 제거 조건 org.hibernate.hql.internal.ast.QueryTranslatorImpl::list::374
final boolean needsDistincting = (
query.getSelectClause().isDistinct() ||
getEntityGraphQueryHint() != null ||
hasLimit )
&& containsCollectionFetches();