Hello

[JPA] 하위 그래프가 하위 그래프를 사용하는 방법 본문

java

[JPA] 하위 그래프가 하위 그래프를 사용하는 방법

nari0_0 2024. 1. 11. 16:16
728x90

아래구조와 동일한 객체 조회 시 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");

 

문자열 EntityGraph 작성

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을 가진 것 처럼동작한다.

List로 선언

해결 방법

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/

728x90