FetchMode는 SpringData JPA에서 어떻게 작동합니까?


95

내 프로젝트의 세 가지 모델 객체 (게시물 끝에있는 모델 및 저장소 스 니펫)간에 관계가 있습니다.

내가 호출 PlaceRepository.findById하면 세 가지 선택 쿼리가 실행됩니다.

( "sql")

  1. SELECT * FROM place p where id = arg
  2. SELECT * FROM user u where u.id = place.user.id
  3. SELECT * FROM city c LEFT OUTER JOIN state s on c.woj_id = s.id where c.id = place.city.id

그것은 다소 비정상적인 행동입니다. Hibernate 문서를 읽은 후 알 수있는 한 항상 JOIN 쿼리를 사용해야합니다. 클래스 (추가 SELECT가있는 쿼리) 에서로 FetchType.LAZY변경 될 때 쿼리에는 차이가 없으며 , (JOIN을 사용한 쿼리)로 변경 될 때 클래스에 대해서도 동일합니다 .FetchType.EAGERPlaceCityFetchType.LAZYFetchType.EAGER

CityRepository.findById화재 억제를 사용할 때 두 가지 선택 :

  1. SELECT * FROM city c where id = arg
  2. SELECT * FROM state s where id = city.state.id

내 목표는 모든 상황에서 sam 동작을 갖는 것입니다 (항상 JOIN 또는 SELECT, JOIN 선호).

모델 정의 :

장소:

@Entity
@Table(name = "place")
public class Place extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "id_user_author")
    private User author;

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_city_id")
    private City city;
    //getters and setters
}

시티:

@Entity
@Table(name = "area_city")
public class City extends Identified {

    @Fetch(FetchMode.JOIN)
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "area_woj_id")
    private State state;
    //getters and setters
}

저장소 :

PlaceRepository

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    Place findById(int id);
}

UserRepository :

public interface UserRepository extends JpaRepository<User, Long> {
        List<User> findAll();
    User findById(int id);
}

CityRepository :

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    City findById(int id);
}

하바 5 가지 방법에 대해 살펴 게으른 relationsships를 초기화 : thoughts-on-java.org/...는
그리고 리 Kislin에게

답변:


114

SpringData는 FetchMode를 무시한다고 생각합니다. 저는 SpringData로 작업 할 때 항상 @NamedEntityGraph@EntityGraph주석을 사용합니다.

@Entity
@NamedEntityGraph(name = "GroupInfo.detail",
  attributeNodes = @NamedAttributeNode("members"))
public class GroupInfo {

  // default fetch mode is lazy.
  @ManyToMany
  List<GroupMember> members = new ArrayList<GroupMember>();

  …
}

@Repository
public interface GroupRepository extends CrudRepository<GroupInfo, String> {

  @EntityGraph(value = "GroupInfo.detail", type = EntityGraphType.LOAD)
  GroupInfo getByGroupName(String name);

}

여기 에서 문서를 확인 하세요.


1
나는 나를 위해 일하지 않는 것 같습니다. 내 말은 작동하지만 ... '@EntityGraph'로 저장소에 주석을 달면 (보통) 자체적으로 작동하지 않습니다. 예 :`Place findById (int id);`는 작동하지만 List<Place> findAll();Exception으로 끝납니다 org.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!. 수동으로 추가 할 때 작동합니다 @Query("select p from Place p"). 그래도 해결 방법처럼 보입니다.
SirKometa 2015

다른 메소드 "findById"가 런타임에 생성 된 사용자 정의 쿼리 메소드 인 반면 JpaRepository 인터페이스의 기존 메소드이기 때문에 findAll ()에서 작동하지 않을 수 있습니다.
wesker317

나는 이것이 최고이기 때문에 올바른 대답으로 표시하기로 결정했습니다. 그래도 완벽하지는 않습니다. 대부분의 시나리오에서 작동하지만 지금까지 더 복잡한 EntityGraph를 사용하는 spring-data-jpa에서 버그를 발견했습니다. 감사합니다 :)
SirKometa 2015

2
@EntityGraph이 지정 질수 있기 때문에 실제 시나리오에서 거의 ununsable 어떤 종류의 Fetch우리가 사용하고자하는 ( JOIN, SUBSELECT, SELECT, BATCH). 이것은 @OneToMany연관과 결합하여 우리가 query를 사용하더라도 Hibernate가 전체 테이블을 메모리로 가져 오게 만든다 MaxResults.
Ondrej Bozek

1
감사합니다. JPQL 쿼리가 선택 가져 오기 정책 으로 기본 가져 오기 전략을 재정 의 할 수 있다고 말하고 싶었습니다 .
adrhc

53

먼저, @Fetch(FetchMode.JOIN)그리고 @ManyToOne(fetch = FetchType.LAZY)다른 하나는 LAZY가 가져 제안하면서 하나가 열망 인출을 지시, 적대적이다.

즉시 가져 오기는 좋은 선택 이 아니며 예측 가능한 동작을 위해서는 쿼리 시간 JOIN FETCH지시문을 사용하는 것이 좋습니다 .

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {

    @Query(value = "SELECT p FROM Place p LEFT JOIN FETCH p.author LEFT JOIN FETCH p.city c LEFT JOIN FETCH c.state where p.id = :id")
    Place findById(@Param("id") int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom { 
    @Query(value = "SELECT c FROM City c LEFT JOIN FETCH c.state where c.id = :id")   
    City findById(@Param("id") int id);
}

3
Criteria API 및 Spring Data Specifications로 동일한 결과를 얻을 수있는 방법이 있습니까?
svlada

2
JPA 가져 오기 프로필이 필요한 가져 오기 부분이 아닙니다.
Vlad Mihalcea 2015

Vlad Mihalcea, SpringData JPA 기준 (사양)을 사용하여이를 수행하는 방법에 대한 예제와 링크를 공유 할 수 있습니까? 제발
얀 Khonski

그러한 예제는 없지만 SpringData JPA 튜토리얼에서 확실히 찾을 수 있습니다.
Vlad Mihalcea

query-time .....을 사용하는 경우 엔티티에 @OneToMany ... etc를 정의해야합니까?
에릭 황

19

Spring-jpa는 엔티티 관리자를 사용하여 쿼리를 생성하고, 쿼리가 엔티티 관리자에 의해 빌드 된 경우 Hibernate는 가져 오기 모드를 무시합니다.

다음은 내가 사용한 해결 방법입니다.

  1. SimpleJpaRepository에서 상속하는 사용자 정의 저장소 구현

  2. 메서드 재정의 getQuery(Specification<T> spec, Sort sort):

    @Override
    protected TypedQuery<T> getQuery(Specification<T> spec, Sort sort) { 
        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
        CriteriaQuery<T> query = builder.createQuery(getDomainClass());
    
        Root<T> root = applySpecificationToCriteria(spec, query);
        query.select(root);
    
        applyFetchMode(root);
    
        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }
    
        return applyRepositoryMethodMetadata(entityManager.createQuery(query));
    }
    

    메서드 중간에 추가 applyFetchMode(root);하여 페치 모드를 적용하고 Hibernate가 올바른 조인으로 쿼리를 생성하도록합니다.

    (안타깝게도 다른 확장 점이 없기 때문에 기본 클래스에서 전체 메서드 및 관련 개인 메서드를 복사해야합니다.)

  3. 구현 applyFetchMode:

    private void applyFetchMode(Root<T> root) {
        for (Field field : getDomainClass().getDeclaredFields()) {
    
            Fetch fetch = field.getAnnotation(Fetch.class);
    
            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                root.fetch(field.getName(), JoinType.LEFT);
            }
        }
    }
    

불행히도 이것은 저장소 메서드 이름을 사용하여 생성 된 쿼리에 대해서는 작동하지 않습니다.
Ondrej Bozek

수입 명세서를 모두 추가해 주시겠습니까? 감사합니다.
granadaCoder

3

" FetchType.LAZY"은 기본 테이블에 대해서만 실행됩니다. 코드에서 부모 테이블 종속성이있는 다른 메서드를 호출하면 해당 테이블 정보를 가져 오는 쿼리가 실행됩니다. (FIRES MULTIPLE SELECT)

" FetchType.EAGER"은 관련 상위 테이블을 포함한 모든 테이블의 조인을 직접 생성합니다. (사용 JOIN)

사용시기 : 종속 상위 테이블 정보를 강제로 사용해야한다고 가정하고 FetchType.EAGER. 특정 기록에 대한 정보 만 필요한 경우 FetchType.LAZY.

기억하세요, FetchType.LAZY당신이 부모 테이블 정보를 검색하도록 선택하는 경우 코드에서 장소에서 활성 DB 세션 공장을 필요로한다.

LAZY:

.. Place fetched from db from your dao loayer
.. only place table information retrieved
.. some code
.. getCity() method called... Here db request will be fired to get city table info

추가 참조


흥미롭게도이 답변은 NamedEntityGraph수화되지 않은 개체 그래프를 원했기 때문에 올바른 사용 경로를 얻었습니다 .
JJ Zabkar

이 답변은 더 많은 찬성 투표를받을 가치가 있습니다. 간결하고 "마 법적으로 트리거 된"쿼리를 많이 보는 이유를 이해하는 데 많은 도움이되었습니다. 감사합니다!
Clint Eastwood

3

객체를 선택할 때 가져 오기 모드에만 작동합니다 ID로 사용하여, 즉 entityManager.find(). SpringData는 항상 쿼리를 생성하므로 가져 오기 모드 구성은 아무 소용이 없습니다. 페치 조인과 함께 전용 쿼리를 사용하거나 엔터티 그래프를 사용할 수 있습니다.

최상의 성능을 원하면 실제로 필요한 데이터의 하위 집합 만 선택해야합니다. 이렇게하려면 일반적으로 불필요한 데이터를 가져 오지 않도록 DTO 접근 방식을 사용하는 것이 좋지만 JPQL을 통해 DTO 모델을 구성하는 전용 쿼리를 정의해야하므로 일반적으로 오류가 발생하기 쉬운 상용구 코드가 많이 발생합니다. 생성자 표현식.

스프링 데이터 프로젝션이 여기에서 도움이 될 수 있지만 어느 시점에서는 Blaze-Persistence Entity Views 와 같은 솔루션이 필요합니다.이 솔루션 은이를 매우 쉽게 만들고 유용한 기능이 더 많이 포함되어 있습니다! 게터가 필요한 데이터의 하위 집합을 나타내는 엔티티별로 DTO 인터페이스를 생성하기 만하면됩니다. 문제에 대한 해결책은 다음과 같습니다.

@EntityView(Identified.class)
public interface IdentifiedView {
    @IdMapping
    Integer getId();
}

@EntityView(Identified.class)
public interface UserView extends IdentifiedView {
    String getName();
}

@EntityView(Identified.class)
public interface StateView extends IdentifiedView {
    String getName();
}

@EntityView(Place.class)
public interface PlaceView extends IdentifiedView {
    UserView getAuthor();
    CityView getCity();
}

@EntityView(City.class)
public interface CityView extends IdentifiedView {
    StateView getState();
}

public interface PlaceRepository extends JpaRepository<Place, Long>, PlaceRepositoryCustom {
    PlaceView findById(int id);
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<UserView> findAllByOrderByIdAsc();
    UserView findById(int id);
}

public interface CityRepository extends JpaRepository<City, Long>, CityRepositoryCustom {    
    CityView findById(int id);
}

면책 조항, 저는 Blaze-Persistence의 저자이므로 편견이있을 수 있습니다.


2

중첩 된 Hibernate 주석을 처리하도록 dream83619 답변에 대해 자세히 설명했습니다 @Fetch. 중첩 된 관련 클래스에서 주석을 찾기 위해 재귀 적 방법을 사용했습니다.

따라서 사용자 지정 저장소를 구현 하고 getQuery(spec, domainClass, sort)메서드를 재정의해야 합니다. 불행히도 참조 된 모든 private 메서드를 복사해야합니다.

다음은 코드입니다. 복사 된 private 메서드는 생략되었습니다.
편집 : 나머지 개인 메서드를 추가했습니다.

@NoRepositoryBean
public class EntityGraphRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> {

    private final EntityManager em;
    protected JpaEntityInformation<T, ?> entityInformation;

    public EntityGraphRepositoryImpl(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.em = entityManager;
        this.entityInformation = entityInformation;
    }

    @Override
    protected <S extends T> TypedQuery<S> getQuery(Specification<S> spec, Class<S> domainClass, Sort sort) {
        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

        query.select(root);
        applyFetchMode(root);

        if (sort != null) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return applyRepositoryMethodMetadata(em.createQuery(query));
    }

    private Map<String, Join<?, ?>> joinCache;

    private void applyFetchMode(Root<? extends T> root) {
        joinCache = new HashMap<>();
        applyFetchMode(root, getDomainClass(), "");
    }

    private void applyFetchMode(FetchParent<?, ?> root, Class<?> clazz, String path) {
        for (Field field : clazz.getDeclaredFields()) {
            Fetch fetch = field.getAnnotation(Fetch.class);

            if (fetch != null && fetch.value() == FetchMode.JOIN) {
                FetchParent<?, ?> descent = root.fetch(field.getName(), JoinType.LEFT);
                String fieldPath = path + "." + field.getName();
                joinCache.put(path, (Join) descent);

                applyFetchMode(descent, field.getType(), fieldPath);
            }
        }
    }

    /**
     * Applies the given {@link Specification} to the given {@link CriteriaQuery}.
     *
     * @param spec can be {@literal null}.
     * @param domainClass must not be {@literal null}.
     * @param query must not be {@literal null}.
     * @return
     */
    private <S, U extends T> Root<U> applySpecificationToCriteria(Specification<U> spec, Class<U> domainClass,
        CriteriaQuery<S> query) {

        Assert.notNull(query);
        Assert.notNull(domainClass);
        Root<U> root = query.from(domainClass);

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
        if (getRepositoryMethodMetadata() == null) {
            return query;
        }

        LockModeType type = getRepositoryMethodMetadata().getLockModeType();
        TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

        applyQueryHints(toReturn);

        return toReturn;
    }

    private void applyQueryHints(Query query) {
        for (Map.Entry<String, Object> hint : getQueryHints().entrySet()) {
            query.setHint(hint.getKey(), hint.getValue());
        }
    }

    public Class<T> getEntityType() {
        return entityInformation.getJavaType();
    }

    public EntityManager getEm() {
        return em;
    }
}

솔루션을 시도하고 있지만 문제를 일으키는 복사 방법 중 하나에 개인 메타 데이터 변수가 있습니다. 최종 코드를 공유 할 수 있습니까?
Homer1980ar

재귀 가져 오기가 작동하지 않습니다. 내가 OneToMany있는 경우는 다음 반복에 java.util.List를 전달
antohoho

아직 잘 테스트하지는 않았지만 재귀 적으로 applyFetchMode를 호출 할 때 field.getType () 대신 ((Join) descent) .getJavaType ()과 같은 것이되어야한다고 생각하십시오
antohoho

2


이 링크에서 http://jdpgrailsdev.github.io/blog/2014/09/09/spring_data_hibernate_join.html :

Hibernate에서 JPA를 사용하는 경우 Hibernate에서 사용하는 FetchMode를 JOIN으로 설정할 방법이 없지만 Hibernate에서 JPA를 사용하는 경우 Hibernate에서 사용하는 FetchMode를 JOIN으로 설정할 수있는 방법이 없습니다.

SpringData JPA 라이브러리는 생성 된 쿼리의 동작을 제어 할 수있는 Domain Driven Design Specifications API를 제공합니다.

final long userId = 1;

final Specification<User> spec = new Specification<User>() {
   @Override
    public Predicate toPredicate(final Root<User> root, final 
     CriteriaQuery<?> query, final CriteriaBuilder cb) {
    query.distinct(true);
    root.fetch("permissions", JoinType.LEFT);
    return cb.equal(root.get("id"), userId);
 }
};

List<User> users = userRepository.findAll(spec);

2

Vlad Mihalcea에 따르면 ( https://vladmihalcea.com/hibernate-facts-the-importance-of-fetch-strategy/ 참조 ) :

JPQL 쿼리는 기본 가져 오기 전략을 재정의 할 수 있습니다. inner 또는 left join fetch 지시문을 사용하여 가져올 항목을 명시 적으로 선언하지 않으면 기본 선택 가져 오기 정책이 적용됩니다.

JPQL 쿼리가 선언 된 가져 오기 전략을 재정의 할 수 있으므로 join fetch참조 된 항목을 열심히로드하거나 단순히 EntityManager를 사용하여 ID로로드하기 위해 사용해야 합니다 (인출 전략을 따르지만 사용 사례에 대한 솔루션이 아닐 수 있음). ).

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.