删除然后创建记录导致与 Spring Data JPA 的重复键冲突
Delete then create records are causing a duplicate key violation with Spring Data JPA
所以,我有这样的场景,我需要获取 header 记录,删除它的详细信息,然后 re-create 以不同的方式删除详细信息。更新详情太麻烦了
我基本上有:
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// header is found, has multiple details
// Remove the details
for(Detail detail : header.getDetails()) {
header.getDetails().remove(detail);
}
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
header.getDetails().add(detail);
}
headerService.save(header);
}
现在,数据库有如下约束:
Header
=================================
ID, other columns...
Detail
=================================
ID, HEADER_ID, CUSTOMER_ID
Customer
=================================
ID, other columns...
Constraint: Details must be unique by HEADER_ID and CUSTOMER_ID so:
Detail (VALID)
=================================
1, 123, 10
2, 123, 12
Detail (IN-VALID)
=================================
1, 123, 10
1, 123, 10
好的,当我 运行 这个并传入 2、3、20 等客户时,它会创建所有 Detail
记录,只要之前没有任何记录。
如果我再次 运行 传递不同的客户列表,我希望首先删除 ALL
详细信息,然后创建 NEW
详细信息列表。
但实际情况是删除似乎在创建之前没有得到遵守。因为错误是一个重复的键约束。重复键就是上面的"IN-VALID"场景
如果我用一堆详细信息手动填充数据库并注释掉 CREATE details
部分(仅 运行 删除),那么记录就被删除了。所以删除有效。创建作品。只是两者不能一起工作。
我可以提供更多需要的代码。我正在使用 Spring Data JPA
.
谢谢
更新
我的实体基本上用以下注释:
@Entity
@Table
public class Header {
...
@OneToMany(mappedBy = "header", orphanRemoval = true, cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
private Set<Detail> Details = new HashSet<>();
...
}
@Entity
@Table
public class Detail {
...
@ManyToOne(optional = false)
@JoinColumn(name = "HEADER_ID", referencedColumnName = "ID", nullable = false)
private Header header;
...
}
更新 2
@Klaus Groenbaek
其实我一开始并没有提到这个,但我第一次就这么说了。此外,我正在使用 Cascading.ALL,我认为它包括 PERSIST。
为了测试,我已将代码更新为以下内容:
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// Remove the details
detailRepository.delete(header.getDetails()); // Does not work
// I've also tried this:
for(Detail detail : header.getDetails()) {
detailRepository.delete(detail);
}
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
detail.setHeader(header);
detailRepository.save(detail)
}
}
再次......我想重申......如果我没有立即创建,删除将起作用。如果我没有紧接在它之前的删除,则创建将起作用。但是由于数据库中的重复键约束错误,如果它们在一起,它们都不会起作用。
我已经尝试过使用和不使用级联删除的相同场景。
首先,只是执行 header.getDetails().remove(detail);
不要对数据库执行任何类型的操作。我想在 headerService.save(header);
中你会调用类似 session.saveOrUpdate(header)
.
的东西
基本上这是某种逻辑冲突,因为 Hibernate 需要在一次操作中删除和创建具有重复键的实体,但是它不知道这些操作的顺序应该执行。
我建议至少调用 headerService.save(header);
before 添加新的细节,即像这样:
// Remove the details
for(Detail detail : header.getDetails()) {
header.getDetails().remove(detail);
}
headerService.save(header);
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
// ....
}
headerService.save(header);
为了告诉 Hibernate:是的,删除我已从集合中删除的这个实体,然后添加新实体。
坚持你的帽子,因为这是一个相当长的解释,但当我查看你的代码时,你似乎遗漏了几个关于 JPA 工作原理的关键概念。
首先,将实体添加到 collection 或从 collection 中删除实体并不意味着相同的操作将发生在数据库中,除非使用级联或 orphanRemoval 传播持久性操作.
要将实体添加到数据库,您必须直接调用 EntityManager.persist()
或通过级联持久化。这基本上就是 JPARepository.save()
里面发生的事情
如果您希望删除一个实体,您必须直接调用 EntityManager.remove()
或通过级联操作,或通过 JpaRepository.delete()
.
如果您有一个托管实体(加载到持久性上下文中的实体),并且您修改了事务中的基本字段(non-entity、non-collection),那么此更改将写入在事务提交时发送到数据库,即使您没有调用 persist/save
。持久性上下文保留每个加载实体的内部副本,当事务提交时,它循环遍历内部副本并与当前状态进行比较,任何基本的归档更改都会触发更新查询。
如果您已将新实体 (A) 添加到另一个实体 (B) 上的 collection,但尚未对 A 调用持久化,则 A 将不会保存到数据库中。如果你在 B 上调用 persist 会发生两种情况之一,如果 persist 操作是级联的,A 也会被保存到数据库中。如果 persist 没有级联,你会得到一个错误,因为一个托管实体引用一个非托管实体,它在 EclipseLink 上给出这个错误:"During synchronization a new object was found through a relationship that was not marked cascade PERSIST"。 Cascade persist 是有道理的,因为您经常创建一个 parent 实体并且它同时是 children。
当您想从另一个实体 B 上的 collection 中删除实体 A 时,您不能依赖级联,因为您没有删除 B。相反,您必须直接对 A 调用删除,从 B 上的 collection 中删除它没有任何效果,因为没有在 EntityManager 上调用持久性操作。您也可以使用 orphanRemoval 来触发删除,但我建议您在使用此功能时要小心,尤其是因为您似乎缺少有关持久性操作如何工作的一些基本知识。
通常考虑持久性操作以及它必须应用于哪个实体会有所帮助。如果我编写代码,代码会是这样的。
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// header is found, has multiple details
// Remove the details
for(Detail detail : header.getDetails()) {
em.remove(detail);
}
// em.flush(); // In some case you need to flush, see comments below
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
detail.setHeader(header); // did this happen inside you service?
em.persist(detail);
}
}
首先,没有理由持久化Header,它是一个托管实体,您修改的任何基本字段都将在事务提交时更改。 Header 恰好是 Details 实体的外键,这意味着重要的是 detail.setHeader(header);
和 em.persist(details)
,因为您必须设置所有外关系,并保留任何新的 Details
.
同样,从 Header 中删除现有详细信息与 Header 无关,定义关系(外键)在详细信息中,因此从持久性上下文中删除详细信息就是将其从数据库中删除.您也可以使用 orphanRemoval,但这需要为每个事务添加额外的逻辑,并且在我看来,如果每个持久化操作都是显式的,那么代码更容易阅读,这样您就不需要返回到实体来阅读注释。
最后:您代码中的持久化操作顺序不会转换为对数据库执行查询的顺序。 Hibernate 和 EclipseLink 都会先插入新实体,然后删除现有实体。根据我的经验,这是 "Primary key already exist" 最常见的原因。如果您删除具有特定主键的实体,然后添加具有相同主键的新实体,则插入将首先发生,并导致键冲突。这可以通过告诉 JPA 将当前持久性状态刷新到数据库来解决。 em.flush()
会将删除查询推送到数据库,因此您可以插入与已删除的主键相同的另一行。
信息量很大,如果有什么不明白的,或者需要我解释的,请告诉我。
@klaus-groenbaek 描述了原因,但我在处理它时注意到一些有趣的事情。
在使用 Spring JpaRepository
时,我无法在使用派生方法时使用它。
因此以下内容不起作用:
void deleteByChannelId(Long channelId);
但是指定显式 (Modifying
) Query
可以使其正常工作,因此以下工作:
@Modifying
@Query("delete from ClientConfigValue v where v.channelId = :channelId")
void deleteByChannelId(@Param("channelId") Long channelId);
在这种情况下,语句以正确的顺序提交/持久化。
所以,我有这样的场景,我需要获取 header 记录,删除它的详细信息,然后 re-create 以不同的方式删除详细信息。更新详情太麻烦了
我基本上有:
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// header is found, has multiple details
// Remove the details
for(Detail detail : header.getDetails()) {
header.getDetails().remove(detail);
}
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
header.getDetails().add(detail);
}
headerService.save(header);
}
现在,数据库有如下约束:
Header
=================================
ID, other columns...
Detail
=================================
ID, HEADER_ID, CUSTOMER_ID
Customer
=================================
ID, other columns...
Constraint: Details must be unique by HEADER_ID and CUSTOMER_ID so:
Detail (VALID)
=================================
1, 123, 10
2, 123, 12
Detail (IN-VALID)
=================================
1, 123, 10
1, 123, 10
好的,当我 运行 这个并传入 2、3、20 等客户时,它会创建所有 Detail
记录,只要之前没有任何记录。
如果我再次 运行 传递不同的客户列表,我希望首先删除 ALL
详细信息,然后创建 NEW
详细信息列表。
但实际情况是删除似乎在创建之前没有得到遵守。因为错误是一个重复的键约束。重复键就是上面的"IN-VALID"场景
如果我用一堆详细信息手动填充数据库并注释掉 CREATE details
部分(仅 运行 删除),那么记录就被删除了。所以删除有效。创建作品。只是两者不能一起工作。
我可以提供更多需要的代码。我正在使用 Spring Data JPA
.
谢谢
更新
我的实体基本上用以下注释:
@Entity
@Table
public class Header {
...
@OneToMany(mappedBy = "header", orphanRemoval = true, cascade = {CascadeType.ALL}, fetch = FetchType.EAGER)
private Set<Detail> Details = new HashSet<>();
...
}
@Entity
@Table
public class Detail {
...
@ManyToOne(optional = false)
@JoinColumn(name = "HEADER_ID", referencedColumnName = "ID", nullable = false)
private Header header;
...
}
更新 2
@Klaus Groenbaek
其实我一开始并没有提到这个,但我第一次就这么说了。此外,我正在使用 Cascading.ALL,我认为它包括 PERSIST。
为了测试,我已将代码更新为以下内容:
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// Remove the details
detailRepository.delete(header.getDetails()); // Does not work
// I've also tried this:
for(Detail detail : header.getDetails()) {
detailRepository.delete(detail);
}
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
detail.setHeader(header);
detailRepository.save(detail)
}
}
再次......我想重申......如果我没有立即创建,删除将起作用。如果我没有紧接在它之前的删除,则创建将起作用。但是由于数据库中的重复键约束错误,如果它们在一起,它们都不会起作用。
我已经尝试过使用和不使用级联删除的相同场景。
首先,只是执行 header.getDetails().remove(detail);
不要对数据库执行任何类型的操作。我想在 headerService.save(header);
中你会调用类似 session.saveOrUpdate(header)
.
基本上这是某种逻辑冲突,因为 Hibernate 需要在一次操作中删除和创建具有重复键的实体,但是它不知道这些操作的顺序应该执行。
我建议至少调用 headerService.save(header);
before 添加新的细节,即像这样:
// Remove the details
for(Detail detail : header.getDetails()) {
header.getDetails().remove(detail);
}
headerService.save(header);
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
// ....
}
headerService.save(header);
为了告诉 Hibernate:是的,删除我已从集合中删除的这个实体,然后添加新实体。
坚持你的帽子,因为这是一个相当长的解释,但当我查看你的代码时,你似乎遗漏了几个关于 JPA 工作原理的关键概念。
首先,将实体添加到 collection 或从 collection 中删除实体并不意味着相同的操作将发生在数据库中,除非使用级联或 orphanRemoval 传播持久性操作.
要将实体添加到数据库,您必须直接调用 EntityManager.persist()
或通过级联持久化。这基本上就是 JPARepository.save()
如果您希望删除一个实体,您必须直接调用 EntityManager.remove()
或通过级联操作,或通过 JpaRepository.delete()
.
如果您有一个托管实体(加载到持久性上下文中的实体),并且您修改了事务中的基本字段(non-entity、non-collection),那么此更改将写入在事务提交时发送到数据库,即使您没有调用 persist/save
。持久性上下文保留每个加载实体的内部副本,当事务提交时,它循环遍历内部副本并与当前状态进行比较,任何基本的归档更改都会触发更新查询。
如果您已将新实体 (A) 添加到另一个实体 (B) 上的 collection,但尚未对 A 调用持久化,则 A 将不会保存到数据库中。如果你在 B 上调用 persist 会发生两种情况之一,如果 persist 操作是级联的,A 也会被保存到数据库中。如果 persist 没有级联,你会得到一个错误,因为一个托管实体引用一个非托管实体,它在 EclipseLink 上给出这个错误:"During synchronization a new object was found through a relationship that was not marked cascade PERSIST"。 Cascade persist 是有道理的,因为您经常创建一个 parent 实体并且它同时是 children。
当您想从另一个实体 B 上的 collection 中删除实体 A 时,您不能依赖级联,因为您没有删除 B。相反,您必须直接对 A 调用删除,从 B 上的 collection 中删除它没有任何效果,因为没有在 EntityManager 上调用持久性操作。您也可以使用 orphanRemoval 来触发删除,但我建议您在使用此功能时要小心,尤其是因为您似乎缺少有关持久性操作如何工作的一些基本知识。
通常考虑持久性操作以及它必须应用于哪个实体会有所帮助。如果我编写代码,代码会是这样的。
@Transactional
public void create(Integer id, List<Integer> customerIDs) {
Header header = headerService.findOne(id);
// header is found, has multiple details
// Remove the details
for(Detail detail : header.getDetails()) {
em.remove(detail);
}
// em.flush(); // In some case you need to flush, see comments below
// Iterate through list of ID's and create Detail with other objects
for(Integer id : customerIDs) {
Customer customer = customerService.findOne(id);
Detail detail = new Detail();
detail.setCustomer(customer);
detail.setHeader(header); // did this happen inside you service?
em.persist(detail);
}
}
首先,没有理由持久化Header,它是一个托管实体,您修改的任何基本字段都将在事务提交时更改。 Header 恰好是 Details 实体的外键,这意味着重要的是 detail.setHeader(header);
和 em.persist(details)
,因为您必须设置所有外关系,并保留任何新的 Details
.
同样,从 Header 中删除现有详细信息与 Header 无关,定义关系(外键)在详细信息中,因此从持久性上下文中删除详细信息就是将其从数据库中删除.您也可以使用 orphanRemoval,但这需要为每个事务添加额外的逻辑,并且在我看来,如果每个持久化操作都是显式的,那么代码更容易阅读,这样您就不需要返回到实体来阅读注释。
最后:您代码中的持久化操作顺序不会转换为对数据库执行查询的顺序。 Hibernate 和 EclipseLink 都会先插入新实体,然后删除现有实体。根据我的经验,这是 "Primary key already exist" 最常见的原因。如果您删除具有特定主键的实体,然后添加具有相同主键的新实体,则插入将首先发生,并导致键冲突。这可以通过告诉 JPA 将当前持久性状态刷新到数据库来解决。 em.flush()
会将删除查询推送到数据库,因此您可以插入与已删除的主键相同的另一行。
信息量很大,如果有什么不明白的,或者需要我解释的,请告诉我。
@klaus-groenbaek 描述了原因,但我在处理它时注意到一些有趣的事情。
在使用 Spring JpaRepository
时,我无法在使用派生方法时使用它。
因此以下内容不起作用:
void deleteByChannelId(Long channelId);
但是指定显式 (Modifying
) Query
可以使其正常工作,因此以下工作:
@Modifying
@Query("delete from ClientConfigValue v where v.channelId = :channelId")
void deleteByChannelId(@Param("channelId") Long channelId);
在这种情况下,语句以正确的顺序提交/持久化。