web-dev-qa-db-ja.com

Hibernateは重複を@OneToManyコレクションに挿入します

Hibernate3.6.7とJPA2.0について質問があります。

次のエンティティを検討してください(簡潔にするために、一部のゲッターとセッターは省略されています)。

@Entity
public class Parent {
    @Id
    @GeneratedValue
    private int id;

    @OneToMany(mappedBy="parent")
    private List<Child> children = new LinkedList<Child>();

    @Override
    public boolean equals(Object obj) {
        return id == ((Parent)obj).id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

@Entity
public class Child {
    @Id
    @GeneratedValue
    private int id;

    @ManyToOne
    private Parent parent;

    public void setParent(Parent parent) {
        this.parent = parent;
    }

    @Override
    public boolean equals(Object obj) {
        return id == ((Child)obj).id;
    }

    @Override
    public int hashCode() {
        return id;
    }
}

ここで、このコードについて考えてみましょう。

// persist parent entity in a transaction

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Parent parent = new Parent();
em.persist(parent);
int id = parent.getId();

em.getTransaction().commit();
em.close();

// relate and persist child entity in a new transaction

em = emf.createEntityManager();
em.getTransaction().begin();

parent = em.find(Parent.class, id);
// *: parent.getChildren().size();
Child child = new Child();
child.setParent(parent);
parent.getChildren().add(child);
em.persist(child);

System.out.println(parent.getChildren()); // -> [Child@1, Child@1]

em.getTransaction().commit();
em.close();

子エンティティが、親エンティティの子のリストに誤って2回挿入されています。

次のいずれかを実行すると、コードは正常に機能します(リストに重複するエントリはありません)。

  • 親エンティティのmappedBy属性を削除します
  • 子のリストに対して読み取り操作を実行します(例:*でマークされたコメント解除行)

これは明らかに非常に奇妙な動作です。また、永続性プロバイダーとしてEclipseLinkを使用する場合、コードは期待どおりに機能します(重複はありません)。

これはHibernateのバグですか、それとも何かが足りませんか?

ありがとう

19
user1014562

これはHibernateのバグです。驚いたことに、それはまだ報告されていません、 それを報告してください

初期化されていないレイジーコレクションに対する操作は、コレクションが初期化された後に実行するためにキューに入れられ、Hibernateは、これらの操作がデータベースのデータと競合する場合の状況を処理しません。通常、このキューはflush()でクリアされ、競合する可能性のある変更がflush()でもデータベースに伝播されるため、問題はありません。ただし、一部の変更(タイプIDENTITYのジェネレーターによって生成されたIDを持つエンティティの永続化など)は、完全なflush()なしでデータベースに伝播されます。ケースの競合が発生する可能性があります。

回避策として、子を永続化した後にセッションをflush()することができます。

em.persist(child); 
em.flush();
27
axtavt

@OneToManyアノテーションが付けられたリストにアイテムを追加する際に問題が発生したときではなく、そのようなリストのアイテムを反復処理しようとしたときに、この質問に遭遇しました。リスト内の項目は常に重複しており、場合によっては2回以上重複していました。 (@ManyToManyアノテーションが付けられたときにも発生しました)。これらのリストでは要素の重複が許可されるはずだったため、ここではセットの使用は解決策ではありませんでした。

例:

_@OneToMany(mappedBy = "parent", fetch = FetchType.EAGER)
@Cascade(CascadeType.ALL)
@LazyCollection(LazyCollectionOption.FALSE)
private List entities;
_

結局のところ、Hibernateは_left outer join_を使用してSQLステートメントを実行します。これにより、dbによって結果が重複して返される可能性があります。 OrderColumnを使用して、結果の順序を定義するだけで役立ちました。

@OrderColumn(name = "columnName")
2
xor_eq

コレクションに重複を追加しないようにHibernateに指示することで、この問題を修正しました。あなたの場合、childrenフィールドのタイプを_List<Child>_から_Set<Child>_に変更し、Childクラスにequals(Object obj)hashCode()を実装します。

明らかに、これはすべての場合に可能であるとは限りませんが、Childインスタンスが一意であることを識別するための適切な方法がある場合、このソリューションは比較的簡単です。

2
carbontax

Java Wildfly(8.2.0-Final)のエンタープライズコンテキスト(Hibernateバージョン4.3.7だと思います)を使用することによる回避策は、最初に子を永続化して、それに追加することでした。怠惰なコレクション:

...
@TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void test(){
    Child child = new Child();
    child.setParent(parent);
    childFacade.create(child);

    parent.getChildren().add(cild);

    parentFacade.edit(parent);
}
1
McIntosh