web-dev-qa-db-ja.com

JPAおよびHibernateのN + 1問題の解決策は何ですか?

N + 1問題は、1つのクエリが実行されてNレコードを取得し、Nクエリがいくつかのリレーショナルレコードを取得することを理解しています。

しかし、Hibernateではどのように回避できますか?

27
Vipul Agarwal

Contactと多対1の関係を持つクラスManufacturerがあるとします。

この問題を解決するには、初期クエリが、適切に初期化された状態で必要なオブジェクトをロードするために必要なすべてのデータをフェッチするようにします。これを行う1つの方法は、HQLフェッチ結合を使用することです。 HQLを使用します

"from Manufacturer manufacturer join fetch manufacturer.contact contact"

フェッチ文で。これにより、内部結合が行われます。

select MANUFACTURER.id from manufacturer and contact ... from 
MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id

Criteriaクエリを使用すると、同じ結果を取得できます

Criteria criteria = session.createCriteria(Manufacturer.class);
criteria.setFetchMode("contact", FetchMode.EAGER);

sQLを作成します:

select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on 
MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1

どちらの場合も、クエリは、連絡先が初期化されたManufacturerオブジェクトのリストを返します。必要なすべての連絡先とメーカーの情報を返すために実行する必要があるクエリは1つだけです。

詳細については、 問題 および ソリューション へのリンクを参照してください。

29
karim mohsen

問題

N + 1クエリの問題は、アソシエーションの取得を忘れてからアクセスする必要がある場合に発生します。

たとえば、次のJPAクエリがあると仮定します。

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

ここで、PostCommentエンティティを繰り返し、post関連付けをトラバースすると、

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

Hibernateは次のSQLステートメントを生成します。

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

これが、N + 1クエリの問題の生成方法です。

postエンティティを取得するときにPostComment関連付けは初期化されないため、HibernateはセカンダリクエリでPostエンティティを取得する必要があり、N PostCommentエンティティの場合、さらにN個のクエリが実行されます(N + 1クエリの問題)。

修正

この問題に取り組むために最初に行う必要があるのは、 適切なSQLロギングとモニタリング を追加することです。ロギングがなければ、特定の機能の開発中にN + 1クエリの問題に気付かないでしょう。

第二に、それを修正するには、単に JOIN FETCH この問題の原因となっている関係:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "join fetch pc.post p " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();

複数の子の関連付けを取得する必要がある場合は、最初のクエリで1つのコレクションを取得し、2番目のコレクションをセカンダリSQLクエリで取得することをお勧めします。

この問題は、統合テストでキャッチする方が適切です。 自動JUnitアサートを使用して、生成されたSQLステートメントの予想カウントを検証できますdb-utilプロジェクト はすでにこの機能を提供しており、オープンソースであり、Maven Centralで依存関係を利用できます。

21
Vlad Mihalcea

Hibernateの1 + Nのネイティブソリューションは、次のように呼び出されます。

20.1.5。バッチフェッチの使用

バッチフェッチを使用すると、1つのプロキシにアクセスした場合、Hibernateはいくつかの初期化されていないプロキシをロードできます。 バッチフェッチは、遅延選択フェッチ戦略の最適化です。バッチフェッチを構成するには、1)クラスレベルと2)コレクションレベルの2つの方法があります...

これらのQ&Aを確認してください。

注釈を使用すると、次のようにできます。

classレベル:

@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements Java.io.Serializable {...

collectionレベル:

@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl() 

遅延読み込みとバッチフェッチはともに最適化を表します。

  • notはクエリでexplicit fetchingを必要とします
  • 任意の量の参照に適用されます。ルートエンティティがロードされた後に(遅延)タッチされます(クエリで指定されたもののみを明示的にフェッチします)
  • collections1つのコレクションのみがルートクエリで取得できるため)で、問題1 + Nをさらに処理することなく解決しますDISTINCTルート値の取得(チェック: Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct
13
Radim Köhler

@BatchSizeアノテーションをどこにでも追加せずに機能させることもできます。プロパティhibernate.default_batch_fetch_sizeを目的の値に設定するだけで、一括フェッチが可能になります。詳細については、 Hibernate docs を参照してください。

その間、デフォルト(LEGACY)が必要なものではない可能性が高いため、おそらく BatchFetchStyle も変更する必要があります。したがって、バッチフェッチをグローバルに有効にするための完全な構成は次のようになります。

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25

また、提案されたソリューションの1つに結合フェッチが含まれていることにも驚いています。結合フェッチは、従属エンティティがすでにL1またはL2キャッシュにロードされている場合でも、すべての結果行でより多くのデータを転送するため、望ましくありません。したがって、設定して完全に無効にすることをお勧めします

hibernate.max_fetch_depth=0
2
philippn