Hibernate CriteriaはFetchType.EAGERで子を複数回返します


115

Orderリストのあるクラスがあり、次のOrderTransactionsように1対多のHibernateマッピングでマッピングしました。

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

これらOrderのにorderStatusは、次の基準によるフィルタリングに使用されるフィールドもあります。

public List<Order> getOrderForProduct(OrderFilter orderFilter) {
    Criteria criteria = getHibernateSession()
            .createCriteria(Order.class)
            .add(Restrictions.in("orderStatus", orderFilter.getStatusesToShow()));
    return criteria.list();
}

これは機能し、結果は期待どおりです。

さてここで、私の質問は:なぜ、私は明示的にフェッチ・タイプを設定したときにEAGER、やるOrder結果のリストにsが複数回表示されますか?

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
public List<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

新しい設定で同じ結果を得るには、どのように基準コードを変更する必要がありますか?


1
show_sqlを有効にして、下で何が起こっているかを確認しましたか?
Mirko N.

OrderTransactionおよびOrde​​rクラスコードも追加してください。\
Eran Medan

回答:


115

構成を正しく理解していれば、これは実際に予想される動作です。

Orderどのインスタンスでも同じインスタンスを取得しますが、今から、OrderTransaction、通常のSQL結合が返すのと同じ量の結果を返す必要があります

したがって、実際に複数回出現するはずです。これは作者(ギャビン・キング)自身がここで非常によく説明しています:それは両方とも、なぜ、そしてまだ明確な結果を得る方法を説明しています


Hibernate FAQにも記載されています:

Hibernateはコレクションに対して外部結合フェッチが有効になっているクエリに対して個別の結果を返しません(たとえ個別のキーワードを使用しても)?まず、SQLと、SQLでOUTER JOINがどのように機能するかを理解する必要があります。SQLの外部結合を完全に理解および理解していない場合は、このFAQ項目を読み続けるのではなく、SQLのマニュアルまたはチュートリアルを参照してください。そうしないと、次の説明が理解できず、Hibernateフォーラムでこの動作について不満を言うでしょう。

同じOrderオブジェクトの重複した参照を返す可能性のある典型的な例:

List result = session.createCriteria(Order.class)
                    .setFetchMode("lineItems", FetchMode.JOIN)
                    .list();

<class name="Order">
    ...
    <set name="lineItems" fetch="join">

List result = session.createCriteria(Order.class)
                       .list();
List result = session.createQuery("select o from Order o left join fetch o.lineItems").list();

これらの例はすべて同じSQLステートメントを生成します。

SELECT o.*, l.* from ORDER o LEFT OUTER JOIN LINE_ITEMS l ON o.ID = l.ORDER_ID

重複が存在する理由を知りたいですか?SQL結果セットを見ると、Hibernateは外部結合された結果の左側にあるこれらの重複を非表示にせず、駆動テーブルのすべての重複を返します。データベースに5つの注文があり、各注文に3つの品目がある場合、結果セットは15行になります。これらのクエリのJava結果リストには15の要素があり、すべてOrderタイプです。Hibernateによって作成されるOrderインスタンスは5つだけですが、SQL結果セットの重複は、これら5つのインスタンスへの重複した参照として保持されます。この最後の文を理解していない場合は、Javaと、Javaヒープ上のインスタンスとそのようなインスタンスへの参照との違いを読む必要があります。

(なぜ左外部結合ですか?ラインアイテムのない追加注文がある場合、結果セットは右側にNULLが入力された16行で、ラインアイテムデータは他の注文用です。注文が必要な場合でも、それらにはラインアイテムがありませんよね?そうでない場合は、HQLで内部結合フェッチを使用してください)。

Hibernateはデフォルトでこれらの重複する参照を除外しません。一部の人々(あなたではない)は実際にこれを望んでいます。どうすればそれらを除外できますか?

このような:

Collection result = new LinkedHashSet( session.create*(...).list() );

121
次の説明を理解していても、Hibernateフォーラムでこの動作について不平を言う可能性があります。
トムアンダーソン

17
まさにトム、Idはギャビンキングスの傲慢な態度を忘れていました。彼はまた、「Hibernateはデフォルトでこれらの重複する参照をフィルターで除外しません。一部の人々(あなたではない)は実際にこれを望んでいます。
ポールテイラー

16
@TomAndersonはい、そうです。なぜ誰もがそれらの重複を必要とするのでしょうか?
わから

13
はぁ。これは実際にはHibernateの欠陥です。クエリを最適化したいので、マッピングファイルで「select」から「join」に移動します。突然私のコードがいたるところに壊れています。次に、実行して、すべてのDAOを結果トランスフォーマーなどを追加して修正します。ユーザーエクスペリエンス==非常に否定的。奇妙な理由で重複が絶対に好きな人もいることは承知していますが、fetch = "justworkplease"を指定して「これらのオブジェクトをより速くフェッチし、重複でバグを発生させない」と言えないのはなぜですか?
Roman Zenka

@エラン:私も同様の問題に直面しています。重複する親オブジェクトは取得していませんが、応答内の親オブジェクトの数に応じて、各親オブジェクトの子が何度も繰り返されています。この問題の理由は何ですか?
mantri 2017年

93

Eranによる言及に加えて、必要な動作を取得する別の方法は、結果トランスフォーマーを設定することです。

criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

8
これは、Criteriaを使用して2つのコレクション/関連付けをフェッチしようとした場合を除いて、ほとんどの場合に機能します。
JamesD 2012年

42

試す

@Fetch (FetchMode.SELECT) 

例えば

@OneToMany(targetEntity = OrderTransaction.class, fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@Fetch (FetchMode.SELECT)
public List<OrderTransaction> getOrderTransactions() {
return orderTransactions;

}


11
FetchMode.SELECTは、Hibernateによって起動されるSQLクエリの数を増やしますが、ルートエンティティレコードごとに1つのインスタンスのみを保証します。この場合、Hibernateはすべての子レコードの選択を起動します。したがって、パフォーマンスの考慮事項に関して、それを考慮する必要があります。
Bipul 14

1
@BipulKumarはい、ただし、サブオブジェクトにアクセスするために遅延フェッチのセッションを維持する必要があるため、遅延フェッチを使用できない場合のオプションです。
マティ2014

18

ListとArrayListではなくSetとHashSetを使用してください。

@OneToMany(targetEntity = OrderTransaction.class, cascade = CascadeType.ALL)
public Set<OrderTransaction> getOrderTransactions() {
    return orderTransactions;
}

2
これは、Hibernateのベストプラクティスの付随的な言及ですか、それともOPからの複数の子の取得に関する質問に関連していますか?
Jacob Zwiers 2014年


とった。OPの質問に次ぐ。しかし、dzoneの記事は、おそらく塩の粒で取られるべきです...コメントでの著者自身の承認に基づいています。
Jacob Zwiers 2014年

2
これは非常に良い答えのIMOです。重複したくない場合は、リストではなくセットを使用する可能性が高いです。セットを使用すると(もちろん、正しいequals / hascodeメソッドを実装すると)、問題が解決します。redhat docで述べられているように、ハッシュコード/等号を実装するときは、idフィールドを使用しないように注意してください。
マット

1
IMOをありがとう。さらに、equals()およびhashCode()メソッドの作成で問題が発生しないようにしてください。IDEまたはLombokにそれらを生成させます。
Αλέκος

3

Java 8とStreamsを使用して、ユーティリティメソッドに次のリターンステートメントを追加します。

return results.stream().distinct().collect(Collectors.toList());

ストリームは非常に高速に複製を削除します。Entityクラスで次のようにアノテーションを使用します。

@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = "STUDENT_COURSES")
private List<Course> courses;

私のアプリでは、データベースからのデータが必要な方法でセッションを使用する方がいいと思います。終わったらセッションを閉じます。もちろん、エンティティクラスを簡易フェッチタイプを使用するように設定しました。リファクタリングに行きます。


3

2つの関連するコレクションを取得するのと同じ問題があります。ユーザーには2つの役割(セット)と2つの食事(リスト)があり、食事が重複しています。

@Table(name = "users")
public class User extends AbstractNamedEntity {

   @CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
   @Column(name = "role")
   @ElementCollection(fetch = FetchType.EAGER)
   @BatchSize(size = 200)
   private Set<Role> roles;

   @OneToMany(fetch = FetchType.LAZY, mappedBy = "user")
   @OrderBy("dateTime DESC")
   protected List<Meal> meals;
   ...
}

DISTINCTは役に立ちません(DATA-JPAクエリ):

@EntityGraph(attributePaths={"meals", "roles"})
@QueryHints({@QueryHint(name= org.hibernate.jpa.QueryHints.HINT_PASS_DISTINCT_THROUGH, value = "false")}) // remove unnecessary distinct from select
@Query("SELECT DISTINCT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

ついに私は2つの解決策を見つけました:

  1. リストをLinkedHashSetに変更
  2. EntityGraphをフィールド "meal"とタイプLOADで使用し、宣言されたとおりにロールをロードします(EAGERおよびBatchSize = 200でN + 1の問題を回避します)。

最終的解決:

@EntityGraph(attributePaths = {"meals"}, type = EntityGraph.EntityGraphType.LOAD)
@Query("SELECT u FROM User u WHERE u.id=?1")
User getWithMeals(int id);

1

次のようなハックを使用する代わりに:

  • Set の代わりに List
  • criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

あなたのSQLクエリを変更しない、私たちは使用できます(引用JPA仕様)

q.select(emp).distinct(true);

これは結果のSQLクエリを変更するため、そのDISTINCT中にがあります。


0

外部結合を適用して重複した結果をもたらす優れた動作には聞こえません。残っている唯一の解決策は、ストリームを使用して結果をフィルタリングすることです。フィルタリングする簡単な方法を提供するjava8に感謝します。

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