[JPA] 하위 그래프가 하위 그래프를 사용하는 방법
아래구조와 동일한 객체 조회 시 lazy 로 동작해 n+1이 발생했다.
특정 쿼리문에서만 join을 사용해 관련된 테이블을 모두 조회해야할 필요가 있어, EntityGraph를 사용해 해결한 방법을 정리하고자 글을 작성합니다.
어떤 게임에서 건축물을 지을 때 해당 건축물은 여러 옵션을 갖고 있다.
건축물은 옵션을 가지며, 옵션은 실제 효과를 여러개 가질 수 있다. 옵션과 효과 객체를 n:m 매핑 테이블을 만들어 사용이 필요하다.
Building 조회시 Option, OptionEffect, Effect를 가진 join 쿼리 적용 방법
- @NamedEntityGraph 정의
@Entity
@Table(name = "tb_building")
@NamedEntityGraph(name = "building.option.effect", attributeNodes = {
@NamedAttributeNode(value = "option", subgraph = "option.effect")
},
subgraphs = {
@NamedSubgraph(name = "option.effect", attributeNodes = {
@NamedAttributeNode(value = "optionEffects", subgraph = "effects")}),
@NamedSubgraph(name = "effects", attributeNodes = {
@NamedAttributeNode(value = "effect")})
})
public class Building {...}
- String으로 EntityGraph 구문 작성
이 글에서는 JPQL 쿼리 작성과 Query Method 두가지 방법에 EntityGraph를 적용하는 내용을 작성합니다.
JPQL 쿼리 작성
@NamedEntityGraph 사용
(EntityManager-getEntityGraph(), createEntityGraph() , EntityGraph - addSubgraph())
1-1) 정의된 EntityGraph를 반환
EntityGraph<?> entityGraph = entityManager.getEntityGraph("building.option.effect");
1-2) 정의된 EntityGraph의 변경 가능한 복사본 반환
EntityGraph<?> entityGraph2 = entityManager.createEntityGraph("building.option.effect");
1-3) EntityGraph를 동적으로 생성하는 데 사용할 수 있는 변경 가능한 EntityGraph를 반환
EntityGraph<Building> entityGraph2 = entityManager.createEntityGraph(Building.class);
entityGraph2.addSubgraph("option")
.addSubgraph("optionEffects")
.addAttributeNodes("effect");
SessionImplementor unwrap = entityManager.unwrap(SessionImplementor.class);
RootGraph<?> entityGraph = GraphParser.parse(Building.class, "option(optionEffects(effect))", unwrap);
쿼리에 entityGraph 적용
@Transactional
public List<Building> entityManagerFindAllByString() {
return entityManager.createQuery("select b from Building b", Building.class)
.setHint("javax.persistence.fetchgraph", entityGraph)
.getResultList();
}
Query Method
public interface BuildingRepo extends JpaRepository<Building, Integer> {
1) 정의된 @NamedEntityGraph 사용
@EntityGraph(value = "building.option.effect")
List<Building> findAll();
2) 문자열 EntityGraph 작성
@EntityGraph(attributePaths = "option.optionEffects.effect")
List<Building> findAll();
}
결과
sql logging을 활성화 하고 쿼리를 실행하면 Hibernate가 Building, Option, OptionEffect, Effect 테이블을 조인하고 4개 엔터티에 매핑된 쿼리를 생성한 것을 확인할 수 있습니다.
EntityGraph 사용 시 주의 사항
위 쿼리는 EntityGraph의 단점을 보여주고 있습니다. 간단한 쿼리에 EntityGraph를 추가 하가해 여러 테이블을 조인하고 많은 컬럼을 읽는 SQL 쿼리가 생성되었습니다. EntityGraph 정의에 주의하고 생성된 SQL 문을 확인하는 것이 중요합니다.
+) OneToMany List 중복 제거
결과는 building id 1,2는 option 1001을 가집니다.
OneToMany관계로 매핑 시 mappedBy("option")로 찾아오는데 이때,
JPA는 building 1일때 option(id:1001), 2일때 option(id:1001) 각각 조회된 데이터가 2개의 option을 가진 것 처럼동작한다.
해결 방법
Set으로 선언해 중복 객체가 담기지 않도록 한다.
List 사용 시 Stream.distincet() 호출해 중복 제거한 리스트를 새로 만든다.
위 케이스의 경우 hibernate5 distinct 옵션 [hibernate.query.passDistinctThrough=false or select distinct x from x] 적용 안됨
+) OneToMany, ManyToMany 컬렉션 관계 fetch join with pagination
setFirstResult().setMaxResults() 이용 시 "쿼리 결과를 메모리에 올려 paging 작업을 어플리케이션 처리한다" 는경고가 발생한다.
-( JQPL 쿼리, Query Method )
참고 :
https://www.inflearn.com/questions/14663/fetch-join-%EC%8B%9C-paging-%EB%AC%B8%EC%A0%9C
https://softwarehut.com/blog/tech/no-lazyinitializationexception-use-jpa-2-1-entity-graph
https://www.baeldung.com/spring-data-jpa-named-entity-graphs
https://thorben-janssen.com/jpa-21-entity-graph-part-1-named-entity/
https://thorben-janssen.com/hibernate-tip-entitygraph-multiple-subgraphs/