使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象
Posted
技术标签:
【中文标题】使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象【英文标题】:A different object with the same identifier using Spring data jpa with Hibernate 【发布时间】:2019-10-06 15:42:45 【问题描述】:我检查了不同的来源,但没有一个能解决我的问题,例如: https://coderanch.com/t/671882/databases/Updating-child-DTO-object-MapsId
Spring + Hibernate : a different object with the same identifier value was already associated with the session
我的案例:我创建了 2 个类,1 个存储库,如下所示:
@Entity
public class Parent
@Id
public long pid;
public String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
public List<Child> children;
-------------------------------------------------------------------
@Entity
public class Child
@EmbeddedId
public PK childPK = new PK();
public String name;
@ManyToOne
@MapsId("parentPk")
@JoinColumn(name = "foreignKeyFromParent")
public Parent parent;
@Embeddable
@EqualsAndHashCode
static class PK implements Serializable
public long parentPk;
public long cid;
------------------------------------------------------------------------
public interface ParentRepository extends JpaRepository<AmazonTest, Long>
Parent 和 Child 具有一对多关系。 在我的主要方法中:
public static void main(String[] args)
@Autowired
private ParentRepository parentRepository;
Parent parent = new Parent();
parent.pid = 1;
parent.name = "Parent 1";
Child child = new Child();
List<Child> childList = new ArrayList<>();
child.childPK.cid = 1;
child.name = "Child 1";
childList.add(child);
parent.children= childList;
parentRepository.save(parent);
parentRepository.flush();
当我第一次运行应用程序时,数据可以成功保存到数据库中。但是如果我再次运行它,它会给出错误“异常:org.springframework.dao.DataIntegrityViolationException:具有相同标识符值的不同对象已与会话相关联”。 我期待如果数据是新的,它会更新我的数据库,如果数据相同,什么都不会发生。我的代码有什么问题。
如果我让父母独立(与孩子没有任何关系)。即使我重新运行应用程序,它也不会给出任何错误。
已编辑:但是,如果我在子实体中使用带有简单主键的以下实现,它将按预期工作。我可以重新运行应用程序而不会出错。我还可以更改值,例如 child.name,它将反映在数据库中。
@Entity
public class Parent
@Id
public long pid;
public String name;
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL)
public List<Child> children;
-------------------------------------------------------------------
@Entity
public class Child
@Id
public long cid;
public String name;
@ManyToOne
@JoinColumn(name = "foreignKeyFromParent")
public Parent parent;
------------------------------------------------------------------------
public interface ParentRepository extends JpaRepository<AmazonTest, Long>
-------------------------------------------------------------------------
public static void main(String[] args)
@Autowired
private ParentRepository parentRepository;
Parent parent = new Parent();
parent.pid = 1;
parent.name = "Parent 1";
Child child = new Child();
List<Child> childList = new ArrayList<>();
child.cid = 1;
child.name = "Child 1";
childList.add(child);
parent.children= childList;
parentRepository.save(parent);
parentRepository.flush();
【问题讨论】:
【参考方案1】:嗯,parent.pid 是您的数据库主键。您只能将一个记录集保存到 id=1 的数据库中。这是预期的行为。
也许让自己熟悉@GeneratedValue 以避免自己设置id。
【讨论】:
如果我只是保存父实例(与子实例没有任何关系)。即使我再次重新运行该应用程序,它也没有显示具有相同 ID 的错误消息。我认为hibernate已经处理了这个问题。在我的问题中编辑了这个 parent.pid 是您数据库中的主键吗?您是否允许具有相同 id 的多个记录? Java 代码中的 @Id 注释不会自动使其成为数据库中的主要注释。如果不是,它将解释您所描述的内容。 是的,我已将 pid 设置为我的数据库中的主键【参考方案2】:在完整解释之前,请注意:尝试发布实际编译和工作的代码。
您的main()
无法编译,
您没有在父母和孩子之间建立完整的关系。
还尝试在发布的示例中明确划分事务。
您的代码是如何工作的
您正在对存储库调用 save。在下面,此方法调用entityManager.merge()
,因为您自己设置了一个 id。 Merge 调用 SQL Select 来验证对象是否存在,然后为对象调用 SQL insert 或 update。 (建议用db中存在id的对象保存是错误的)
在第一次运行中,对象不存在。
您插入父项 合并是级联的,你插入孩子(我们称之为childA
)
第二轮
合并选择父级(使用childA
)
我们比较新父母是否已经在会话中。
这是在SessionImpl.getEntityUsingInterceptor
中完成的
找到父级
merge 级联到子级
我们再次检查对象是否已经在会话中。
现在区别来了:
根据您如何设置子与父之间的关系,子可能有一个不完整的 PK(并依赖于从使用 @MapsId
注释的父关系中填充它)。不幸的是,通过不完整的 PK 在会话中找不到实体,但稍后保存时,PK 已完成,现在,您有 2 个具有相同密钥的冲突对象。
解决它
Child child = new Child();
child.parent = parent;
child.childPK.cid = 1;
child.childPK.parentPk = 1;
这也解释了为什么当您将 Child 的 PK 更改为 long 时代码可以工作 - 没有办法搞砸并有一个不完整的 PK。
注意
上面的解决方案会弄乱孤儿。
我仍然认为原始解决方案更好,因为孤儿被删除了。 此外,将更新的解决方案添加到原始解决方案是值得的更新。 删除整个列表并重新插入它不太可能在负载下表现良好。 不幸的是,它会在父级的第一次合并时删除列表,并在父级的第二次合并时重新添加它们。 (这就是为什么不需要 clear 的原因)
更好的是,只需找到父实体并对其进行更新(如其他答案所示)。
更好的是,尝试查看解决方案并仅添加/替换父项的特定子项,而不是查看父项及其子项。这可能是最高效的。
原始解决方案
我提出以下建议(请注意,不允许完全替换子列表,因为它是休眠代理)。
@OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
public List<Child> children = new ArrayList<>();
@SpringBootTest
public class ParentOrphanRepositoryTest
@Autowired
private ParentOrphanRepository parentOrphanRepository;
@Test
public void testDoubleAdd()
addEntity();
addEntity();
@Transactional
public void addEntity()
Parent parent = new Parent();
parent.pid = 1;
parent.name = "Parent 1";
parent = parentOrphanRepository.save(parent);
Child child = new Child();
List<Child> childList = new ArrayList<>();
child.parent = parent;
child.childPK.cid = 1;
child.name = "Child 1";
childList.add(child);
// parent.children.clear(); Not needed.
parent.children.addAll(childList);
parentOrphanRepository.save(parent);
parentOrphanRepository.flush();
【讨论】:
你能解释一下为什么先保存parent,然后清除一个空数组(我的意思是parent.children.clear())吗? 更新了答案以解决您更新后的映射。and then clear an empty array
。那不是真的。在父节点上合并后,该数组是数据库中所有子节点的惰性代理。
非常感谢,详细的答案和很好的解释。还有2个问题。我使用了原始解决方案而没有清除数组。有用。它实际上创建删除 sql 以清除 db 中的子表。为什么会这样?如何检查数组中的实际元素(所有子代的代理)
抱歉大量更新,但我真的需要一个调试器来检查它。再次更新,原来删除是在第一次合并时发出的(父级有空子级,让我们删除孤儿),并且没有机会使用这种方法将旧父级的列表与新父级的列表合并。找到老父母会给你这样的机会。以上是关于使用带有 Hibernate 的 Spring data jpa 的具有相同标识符的不同对象的主要内容,如果未能解决你的问题,请参考以下文章
带有 Hibernate 和 Ehcache 的 Spring 数据 JPA 不起作用
Spring 3,带有通用 DAO 的 Hibernate 4 AutoWired sessionFactory
Spring Boot / Thymeleaf / Hibernate:带有 Java 注解的 Sessionfactory Bean
带有注释和(动态)AbstractRoutingDataSource 的 Spring 3.1.3 + Hibernate 配置