概要
Spring Data JPA において、下記のような構成のエンティティを作成したところ、MultipleBagFetchException が発生しました。
@Entity
@Table(name = "parent")
@Getter
public class Parent {
@Id
private int id;
@Column(name = "child_id", nullable = false, updatable = false, insertable = false)
private int childId;
@ManyToOne
@JoinColumn(name = "child_id")
private Child child;
}
@Entity
@Table(name = "child")
@Getter
public class Child {
@Id
private int id;
@OneToMany(mappedBy = child)
private List<Parent> parents;
@OneToMany(mappedBy = "child", fetch = FetchType.EAGER)
private List<SubChild1> subChild1;
@OneToMany(mappedBy = "child", fetch = FetchType.EAGER)
protected List<SubChild2> subChild2;
}
@Entity
@Table(name = "sub_child1")
@Getter
public class SubChild1 {
private Child child;
}
@Entity
@Table(name = "sub_child2")
@Getter
public class SubChild2 {
private Child child;
}
Caused by: org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [Parent.child, Child.subChild1, Parent.subChild2]
at org.hibernate.loader.BasicLoader.postInstantiate(BasicLoader.java:75)
詳細
こちらのページと実際の動作から、どうやら、FetchType.EAGER において、Hibernate は 2 層までの Collection のネストを許可していますが、3 層までのネストを許可していないことが問題のようでした。
*JPA allows only two level of nesting for collection, Parententity->Childentity is fine but it does not allow Parententity->childentity->SubChildEntity
*Using List for collection instead of SET -remember a SET guarantees unique elements while list does not
そのため、FetchType.LAZY にてデータを取得しようとした結果、今度は、LazyInitializationException が発生することになりました。
こちらのページ と実際の動作から Lazy で List にアクセスした際、すでに DB のコネクションが閉じてしまっていたために、データの Fetch ができない状況にあるようでした。
どうやらビューでコレクションの要素であるLineItemを遅延初期化(lazily initialize)しようとした時に、 Hinbernateのsessionがクローズしていたので例外が発生みたいです。FetchType.LAZY も立ち行かなくなりました。
そこで、ネット"Spring Hibernate Lazy initialize error" をキーに検索すると
とあるITエンジニアの業務手帖
に同様のケースが報告されていました。
解決策としては、Hibernateマッピングファイルのlazy属性をfalseにするというものでした。 そこで、以下のようにOrderのbagタグにlazy="false"を入れ
解決策
こちらのページ から一部の型を List から Set に変えることで問題が解決できました。
The third solution is to replace java.util.List and java.util.Collection with java.util.Set. We have to think about it - do we really need a List/Collection (a bag)? In many cases the only reason to use a List is that we are used to.
英語力が低く、お恥ずかしい限りですが、Bag は List のように、重複を許容するオブジェクトのようです。
日本語にしてみると、「複数Bags取得例外」となり、まさに現象と一致した動作です。
クラスについては下記のように修正。
@Entity
@Table(name = "child")
@Getter
public class Child {
@Id
private int id;
@OneToMany(mappedBy = child)
private Set<Parent> parents;
@OneToMany(mappedBy = "child", fetch = FetchType.EAGER)
private Set<SubChild1> subChild1;
@OneToMany(mappedBy = "child", fetch = FetchType.EAGER)
protected Set<SubChild2> subChild2;
}
補足 : Set について
公式ページ を確認したところ、Set は重複を許可しないオブジェクトのようです。
重複要素のないコレクションです。すなわち、セットは、e1.equals(e2) である e1 と e2 の要素ペアは持たず、null 要素を最大 1 つしか持ちません。その名前が示すように、このインタフェースは、数学で言う集合の抽象化オブジェクトをモデル化します。
Set インタフェースは、Collection インタフェースから継承した規定だけでなく、すべてのコンストラクタの規約、および add、equals、hashCode の各メソッドの規約に追加の規定を適用します。便宜上、ほかの継承メソッドの宣言もここに含まれます。これらの宣言に付随する仕様は Set インタフェースに合わせて調整済みですが、追加規定は含まれていません。
今回の要件では、重複が発生することはありえませんでしたし、NULL も発生しないのでめでたく解決となりました。