将子对象两次添加到父对象,导致NonUniqueObjectException

时间:2010-11-19 04:54:11

标签: java hibernate jpa

假设我有一个屏幕显示我农场里的动物。如果用户想要添加动物,他们可以单击“添加”按钮,将您带到另一个列出动物名称的屏幕。如果用户选择“dog”,我的应用程序将查询数据库并返回一个Dog对象,然后将其添加到我的Farm实体中的动物集合中。

在上述情况下,如果我要保存Farm实体,则Farm将按预期成功保留。

但是,如果用户向服务器场添加“dog”,然后再决定再添加“dog”,则保存Farm实体会导致:

org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session

现在这很明显是因为用户选择了“狗”两次导致我的应用程序挖掘两个“狗”实例。处理这种情况的最佳方法是什么?

编辑:让我澄清一下,动物没有直接附在农场上。农场将收藏一系列动物保护者,每个动物保护者都可以容纳一只动物。 AnimalShelters是独一无二的,并有自己的标识符。我想你可以把“狗”想象成一只可以在多个避难所之间徘徊的特权狗。

编辑:以下是工作方式的流程:

  • 第一个屏幕显示农场。没有AnimalShelters。
  • 用户点击“添加”按钮添加AnimalShelter
  • 新屏幕上有一个存在于数据库中的动物表
  • 用户使用“查找”选择在数据库中找到的狗[这在交易中完成]
  • 创建一个新的AnimalShelter对象
  • The Dog已设置在新的AnimalShelter
  • 然后用户选择“添加”包含相同Dog的另一个AnimalShelter(重复之前的5个步骤)

编辑:也许我用伪代码解释它可能会让我的问题更加清晰:

  • 公开会话
  • 开始交易
  • Animal animal1 = session.get(Animal.class,1L);
  • 提交交易
  • 将animal1与我们链接到农场的新AnimalShelter链接
  • 开始交易
  • Animal animal2 = session.get(Animal.class,1L); //这会返回同一Animal的不同实例,与第3行中的Animal相比,这是有道理的,因为我们处于不同的事务中。但有没有办法让这个给我相同的Animal实例,即使我在另一个交易?
  • 提交交易
  • 将animal2与我们链接到农场的新AnimalShelter链接 //现在有两个动物收容所,我想指向同一个动物
  • 关闭会话

编辑:以下是架构:

+---------------+
| Farm          |
+---------------+
| Id (pk)       |
| Name          |
+---------------+

+---------------+
| AnimalShelter |
+---------------+
| Id (pk)       |
| AnimalId      |
| FarmId        |
+---------------+

+---------------+
| Animal        |
+---------------+
| Id (pk)       |
| Name          |
+---------------+

编辑: Stacktrace:

org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session: [com.spike.model.Animal#1]
at org.hibernate.engine.StatefulPersistenceContext.checkUniqueness(StatefulPersistenceContext.java:637)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:305)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:246)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:112)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:677)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:669)
at org.hibernate.engine.CascadingAction$5.cascade(CascadingAction.java:252)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:392)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:335)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:204)
at org.hibernate.engine.Cascade.cascade(Cascade.java:161)
at org.hibernate.event.def.AbstractSaveEventListener.cascadeBeforeSave(AbstractSaveEventListener.java:451)
at org.hibernate.event.def.AbstractSaveEventListener.performSaveOrReplicate(AbstractSaveEventListener.java:288)
at org.hibernate.event.def.AbstractSaveEventListener.performSave(AbstractSaveEventListener.java:204)
at org.hibernate.event.def.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:144)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.saveWithGeneratedOrRequestedId(DefaultSaveOrUpdateEventListener.java:210)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsTransient(DefaultSaveOrUpdateEventListener.java:195)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:117)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:677)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:669)
at org.hibernate.engine.CascadingAction$5.cascade(CascadingAction.java:252)
at org.hibernate.engine.Cascade.cascadeToOne(Cascade.java:392)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:335)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:204)
at org.hibernate.engine.Cascade.cascadeCollectionElements(Cascade.java:425)
at org.hibernate.engine.Cascade.cascadeCollection(Cascade.java:362)
at org.hibernate.engine.Cascade.cascadeAssociation(Cascade.java:338)
at org.hibernate.engine.Cascade.cascadeProperty(Cascade.java:204)
at org.hibernate.engine.Cascade.cascade(Cascade.java:161)
at org.hibernate.engine.Cascade.cascade(Cascade.java:127)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.cascadeOnUpdate(DefaultSaveOrUpdateEventListener.java:376)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performUpdate(DefaultSaveOrUpdateEventListener.java:350)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.entityIsDetached(DefaultSaveOrUpdateEventListener.java:246)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.performSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:112)
at org.hibernate.event.def.DefaultSaveOrUpdateEventListener.onSaveOrUpdate(DefaultSaveOrUpdateEventListener.java:93)
at org.hibernate.impl.SessionImpl.fireSaveOrUpdate(SessionImpl.java:677)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:669)
at org.hibernate.impl.SessionImpl.saveOrUpdate(SessionImpl.java:665)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.hibernate.context.ThreadLocalSessionContext$TransactionProtectionWrapper.invoke(ThreadLocalSessionContext.java:344)
at $Proxy14.saveOrUpdate(Unknown Source)
at com.spike.ui.SaveFarmActionListener.actionPerformed(SaveFarmActionListener.java:29)

编辑:从我收到的评论中,我的流程现在看起来像这样仍然会导致抛出相同的异常:

  • 公开会话
  • 开始交易
  • Animal animal1 = session.get(Animal.class,1L);
  • 将animal1与我们链接到农场的新AnimalShelter链接
  • Animal animal2 = session.get(Animal.class,1L); //现在返回与上面相同的动物实例
  • 将animal2与我们链接到农场的新AnimalShelter链接 //现在有两个动物收容所,我想指向同一个动物
  • 提交事务尽管我可以看到它指向同一个Animal实例
  • ,但该语句仍会引发异常
  • 关闭会话

编辑:有趣的是,现在我拥有一个事务中的所有内容,即使我只添加一个Animal,它似乎也会失败并出现相同的异常。这样做的原因似乎是因为当我进入第二个屏幕时,它会查询所有可用的动物 - 其中一个当然是狗。当我只添加一个包含Dog的AnimalShelter并尝试保存时,它将抛出相同的异常,因为我假设它已经被显示所有可用动物的第二个屏幕加载到会话中。

3 个答案:

答案 0 :(得分:1)

然后狗应该有足够的特权与AnimalShelter建立关系。确切地说,AnimalAnimalShelter之间应该存在一对多的关系。

例如覆盖equals()

public boolean equals(Object that) {
    if ( this == that ) return true;
    if ( !(that instanceof Dog) ) return false;
    Dog dog = (Dog)that;
    // Assuming id is of Long type
    return this.id.longValue() == dog.id.longValue();
}

并且不要忘记来覆盖hashcode()

答案 1 :(得分:1)

例外说明了一切:

org.hibernate.NonUniqueObjectException: a different object with the same identifier value was already associated with the session

听起来发生的事情是你将同一实体类的两个对象(我猜是Dog?)引入EntityManager,它们都具有相同的主键。这是不允许的。

如果要创建新的Dog实体(通过创建新实例并在每个实例上调用em.persist()),请确保您不会意外地使用相同的主键值两次。这与尝试使用相同的主键值执行两个本机SQL INSERT语句相同。我猜这是你的问题。 您的应用程序如何为新实体分配新的主键值?您可能会发现主键生成未按预期工作。

如果您正在使用现有的Dog实体,请确保您的实体是“托管”而非“分离”。您可以通过在其上调用em.merge()然后使用merge()调用中返回的(和托管的)实体实例来重新附加分离的实体。我不认为这是你的问题,因为你可能会得到某种类型的分离实体异常而不是“非唯一”异常。

在另一个人的回答中回答您的评论问题:

  

我如何确保处理“狗”   当我对象是同一个“狗”对象时   首先将它添加到AnimalShelter中   时间?

调用EntityManager find()方法,如下所示:

em.find(Dog.class, myDogPrimaryKeyValue)

这将返回由此特定EntityManager“管理”的Dog实例。每次使用相同的主键值调用Dog时,每个EntityManager 保证都会返回find()的同一个实例。请注意,不要尝试使用从不同EntityManager上的一个EntityManager返回的实体实例。实体仅由最初将实体返回给您的EntityManager管理,而不是任何EntityManager。请参阅上面的评论,当您使用非托管(A.K.A.“分离”)实例启动时,使用merge()获取托管实例。您可以通过调用实体上的em.contains()来确定实体实例是否由特定EntityManager管理。

修改

根据问题编辑中的新信息,这似乎是跨越事务边界的问题。您是否可以在会话的整个持续时间内保持事务处于打开状态,而不是执行提交然后创建另一个事务?这可能会解决您的问题。或者,您可以使用应用程序管理的EntityManager,以便持久性上下文在会话期间保持不变。

答案 2 :(得分:1)

我终于得到了它的工作。只是总结一下我从答案和评论中得到的东西,以使其发挥作用:

  • 在对农场进行大量编辑时,所有寻找动物的小动作,将动物与AnimalShelter相关联,将AnimalShelter与农场相关联都是在SINGLE交易中完成的。这允许多次检索动物并确保从会话中返回相同的Animal实例;
  • 在动物实体类中根本没有调用
  • equals()/ hashCode()
  • merge()也不是最终解决方案的一部分
  • 起初我尝试在第二个屏幕上执行session.evict(...)以确保搜索动物不会导致唯一性异常
  • 除了事务之外,实际导致异常的问题的另一个重要部分是我在模型中有双向关系的事实。我在这些关系上设置了级联类型的ALL。在大多数情况下,当它是父母与孩子之间的联系时,级联类型的ALL是有意义的。但我也通过级联ALL将链接返回到父级。一旦我在映射注释中删除了那些无用的(并且最终)不正确的级联属性,异常就消失了!