Spring Data JPAでFetchModeが機能する仕組み


91

プロジェクトの3つのモデルオブジェクト(投稿の最後にあるモデルとリポジトリのスニペット)の間に関係があります。

私が呼び出すPlaceRepository.findByIdと、3つの選択クエリが実行されます。

( "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変更し FetchType.EAGERた場合のクエリに違いはありません。Placeクラス(JOINを使用したクエリ)CityFetchType.LAZY変更した 場合も同様ですFetchType.EAGER

CityRepository.findById火の抑制を使用すると、2つの選択が行われます。

  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);
}

HAVA怠惰relationsshipsを初期化するための5つの方法を見て:thoughts-on-java.org/...
グリゴリーKislin

回答:


109

Spring DataはFetchModeを無視していると思います。Spring Dataを操作するときは常に@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();、例外が発生しますorg.springframework.data.mapping.PropertyReferenceException: No property find found for type Place!。手動で追加すると機能し@Query("select p from Place p")ます。しかし回避策のようです。
SirKometa 2015

JpaRepositoryインターフェースの既存のメソッドであるため、findAll()では機能しない可能性がありますが、他のメソッド「findById」は実行時に生成されるカスタムクエリメソッドです。
wesker317 2015

これが最良なので、これを適切な回答としてマークすることにしました。それは完璧ではありません。ほとんどのシナリオで機能しますが、これまでのところ、より複雑なEntityGraphsを使用したspring-data-jpaのバグに気づきました。ありがとう:)
SirKometa 2015

2
@EntityGraphこれを指定するカントので、実際のシナリオでほとんどununsableでどのような種類のFetch私たちが使いたいです(JOINSUBSELECTSELECTBATCH)。これを@OneToManyアソシエーションと組み合わせると、クエリを使用しても、Hibernate Fetchでテーブル全体がメモリにフェッチされますMaxResults
Ondrej Bozek

1
おかげで、JPQLクエリは、選択フェッチポリシーでデフォルトのフェッチ戦略をオーバーライドする可能性あると言いたいと思います
adrhc

51

まず第一に、@Fetch(FetchMode.JOIN)それ@ManyToOne(fetch = FetchType.LAZY)は敵対的であり、一方はEAGERフェッチを指示し、もう一方はLAZYフェッチを示唆します。

Eager fetchingはめったに良い選択でなく、予測可能な動作のためには、クエリ時間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、Spring Data JPA基準(仕様)を使用してこれを行う方法の例とリンクを共有できますか?お願い
Yan Khonski、2016

そのような例はありませんが、Spring Data JPAチュートリアルできっと見つかります。
Vlad Mihalcea

query-time .....を使用する場合、エンティティで@OneToMany ... etcを定義する必要がありますか?
Eric Huang

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 2017年

すべてのインポートステートメントを追加できますか?ありがとうございました。
granadaCoder

3

FetchType.LAZY」はプライマリテーブルに対してのみ発生します。コード内で親テーブルの依存関係を持つ他のメソッドを呼び出すと、クエリが実行され、そのテーブル情報が取得されます。(火の複数選択)

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

この回答は、より多くの賛成票に値します。それは簡潔であり、「魔法のようにトリガーされた」クエリがたくさん表示された理由を理解するのに大いに役立ちました。
クリントイーストウッド

3

フェッチモードは、IDでオブジェクトを選択する場合にのみ機能しentityManager.find()ます。Spring Dataは常にクエリを作成するため、フェッチモードの設定は不要です。フェッチ結合で専用クエリを使用するか、エンティティグラフを使用できます。

最高のパフォーマンスが必要な場合は、本当に必要なデータのサブセットのみを選択する必要があります。これを行うには、通常、DTOアプローチを使用して不要なデータがフェッチされないようにすることをお勧めしますが、JPQLを介してDTOモデルを構築する専用クエリを定義する必要があるため、通常、エラーが発生しやすいボイラープレートコードが大量に発生します。コンストラクタ式。

Spring Dataプロジェクションはここで役立ちますが、ある時点で、これを非常に簡単にし、便利になるスリーブ内に多くの機能を備えたBlaze-Persistenceエンティティビューのようなソリューションが必要になります。ゲッターが必要なデータのサブセットを表すエンティティごとに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)メソッドを実装する必要があります。残念ながら、参照されているすべてのプライベートメソッドもコピーする必要があります:(。

これがコードで、コピーされたプライベートメソッドは省略されています。
編集:残りのプライベートメソッドが追加されました。

@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;
    }
}

私はあなたの解決策を試していますが、問題を引き起こしているコピー方法の1つにプライベートメタデータ変数があります。最終的なコードを共有できますか?
Homer1980ar 2017

再帰的なフェッチは機能しません。OneToManyがある場合、java.util.Listを次の反復に
渡し

まだ十分にテストされていませんが、再帰的に適用する場合、field.getType()ではなく、((Join)descent).getJavaType()のようになるはずです。applyFetchMode
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に設定する方法はありません。

Spring Data JPAライブラリは、生成されたクエリの動作を制御できるドメイン駆動設計仕様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クエリは、デフォルトのフェッチ戦略をオーバーライドする場合があります。内部または左結合フェッチディレクティブを使用して何をフェッチするかを明示的に宣言しない場合、デフォルトの選択フェッチポリシーが適用されます。

JPQLクエリは宣言されたフェッチ戦略をオーバーライドする可能性があるjoin fetchため、参照エンティティを積極的にロードするか、またはEntityManager を使用してIDで単にロードするために使用する必要があります(フェッチ戦略には従いますが、ユースケースのソリューションではない可能性があります) )。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.