Hibernate在@OneToMany集合中插入重复数据

23

我有一个关于Hibernate 3.6.7和JPA 2.0的问题。

考虑以下实体(为简洁起见,省略了一些getter和setter):

@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();

子实体被错误地插入到父实体的子列表中两次。

当进行以下操作之一时,代码可以正常工作(列表中没有重复项):

  • 在父实体中删除mappedBy属性
  • 对子列表执行某些读取操作(例如取消标有*的行的注释)

这显然是非常奇怪的行为。而且,当使用EclipseLink作为持久性提供程序时,代码就像预期的那样工作(没有重复项)。

这是Hibernate的一个错误还是我遗漏了什么?

谢谢


你能添加setParent方法、equals/hashCode方法的代码吗? - JB Nizet
我刚刚添加了你要求的方法。然而,我不认为这个问题与equals / hashCode有关。 - user1014562
2
你的equals方法不遵守Object.equals的契约。此外,当ID生成并分配给实体时,hashCode也会发生变化。如果你移除hashCode和equals,我不会感到惊讶如果这个bug消失了。顺便说一下,Hibernate建议不要使用ID来实现equals和hashCode。 - JB Nizet
谢谢您的评论。我不知道在equals和hashCode中使用persistent id的争议。但是即使我删除了我的equals和hashCode实现,问题仍然存在。 - user1014562
5个回答

31
这是Hibernate中的一个bug。令人惊讶的是,它尚未被报道,欢迎报告
对未初始化的延迟加载集合进行操作会将操作排队,以便在集合初始化后执行它们,并且当这些操作与数据库中的数据冲突时,Hibernate无法处理该情况。通常情况下,这不是问题,因为此队列会在flush()时清除,并且可能发生冲突的更改也会在flush()时传播到数据库。但是,在某些情况下,(例如生成器类型为IDENTITY的实体持久化,我猜测这就是你的情况),更改会在没有完全flush()的情况下传播到数据库,此时可能会发生冲突���
作为解决方法,您可以在持久化子项后flush()会话。
em.persist(child); 
em.flush();

1
感谢您的回答和解决方法!我已经创建了一个错误报告并引用了您的答案:HHH-6776 - user1014562
这个 bug 还没有解决,真的很奇怪吗?需要在持久化之前读取对象,这太糟糕了。 - nize
1
我在oneToMany中也有重复项,使用列表更改为集合确实有所帮助,但是当我想要使用列表时,为什么我被迫使用集合?使用Hibernate生成的SQL查询不会返回重复项。然而,即使查询没有返回重复项,Hibernate在列表中返回重复项。无法找到不使用集合的解决方案。由于无状态bean,我不需要使用flush。 - nimo23
这个 bug 现在已经在 Hibernate 5.0.8 中修复了:https://hibernate.atlassian.net/browse/HHH-5855 - cst1992
2
我正在使用 Hibernate 5.1.3,问题仍然可以重现。 - Pranit

4

当我尝试遍历用@OneToMany注释的列表中的项时,我遇到了问题,这与向列表中添加项无关。列表中的项总是重复的,有时甚至超过两次。(使用@ManyToMany注释也会发生相同情况)。在这里,使用Set并不是一个解决方案,因为这些列表应该允许其中包含重复元素。

示例:

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

事实证明,Hibernate使用左外连接执行SQL语句,这可能导致数据库返回重复的结果。简单地定义一个结果排序,使用OrderColumn可以帮助解决这个问题:

@OrderColumn(name = "columnName")

无法感谢你的足够,你能指引我一个资源让我可以了解更多关于这个吗? - Sujal Mandal
当使用@OrderColumn进行注释时,Hibernate将使用PersistentOrderedSet,该集合遵守对象的equals()hashCode()。Bug票:在急切加载时,PersistentSet不遵守hashcode/equals契约。来源:https://stackoverflow.com/a/49355892/1125678 - kntx

3
我通过告诉Hibernate不要在我的集合中添加重复项来解决了这个问题。在您的情况下,将children字段的类型从List<Child>更改为Set<Child>,并在Child类上实现equals(Object obj)hashCode()即可。
显然,并非每种情况都可以这样做,但如果有一种明智的方法来确定Child实例是唯一的,则此解决方案可能相对轻松。

1
并设置@OneToMany((...), cascade = CascadeType.MERGE)。 - Hinotori

0

在Wildfly(8.2.0-Final)中使用Java企业上下文(我认为它是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);
}

-2
通过调用 empty() 方法来管理它。对于这种情况,

parent.getChildren().isEmpty()

之前

parent.getChildren().add(child);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接