N + 1問題は、1つのクエリが実行されてNレコードを取得し、Nクエリがいくつかのリレーショナルレコードを取得することを理解しています。
しかし、Hibernateではどのように回避できますか?
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つだけです。
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で依存関係を利用できます。
Hibernateの1 + Nのネイティブソリューションは、次のように呼び出されます。
バッチフェッチを使用すると、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()
遅延読み込みとバッチフェッチはともに最適化を表します。
@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