删除然后创建记录导致与Spring Data JPA的重复键冲突

2022-09-02 09:01:29

因此,我有这种情况,我需要获取标头记录,删除它的详细信息,然后以不同的方式重新创建详细信息。更新细节太麻烦了。

我基本上有:

@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

如果我再次运行它,传入不同的客户列表,我希望首先删除详细信息,然后创建详细信息列表。ALLNEW

但发生的事情是,在创建之前,删除似乎没有得到尊重。因为该错误是重复的键约束。重复的键是上面的“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格隆拜克

实际上,我最初没有提到这一点,但我第一次就这样做了。另外,我正在使用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)
    }
}

再。。。我想重申...如果我之后没有立即创建,则删除将起作用。如果我之前没有删除,则创建将起作用。但是,如果它们在一起,则两者都不起作用,因为数据库中存在重复的键约束错误。

我尝试过相同的场景,有和不带级联删除。


答案 1

抓住你的帽子,因为这是一个相当长的解释,但是当我看你的代码时,看起来你错过了一些关于JPA如何工作的关键概念。

首先,将实体添加到集合或从集合中删除实体并不意味着数据库中将发生相同的操作,除非使用级联或孤立删除传播持久性操作。

对于要添加到数据库的实体,必须直接调用或通过级联持久化调用。这基本上就是内部发生的事情。EntityManager.persist()JPARepository.save()

如果要删除实体,则必须直接调用,也可以通过级联操作调用,或者通过 调用。EntityManager.remove()JpaRepository.delete()

如果您有一个托管实体(加载到持久性上下文中的实体),并且您修改了事务中的基本字段(非实体,非集合),则此更改将在事务提交时写入数据库,即使您没有调用 。持久性上下文保留每个已加载实体的内部副本,当事务提交时,它会遍历内部副本并与当前状态进行比较,并且任何基本的归档更改都会触发更新查询。persist/save

如果已将新实体 (A) 添加到另一个实体 (B) 上的集合中,但尚未在 A 上调用 persist,则不会将 A 保存到数据库中。如果在 B 上调用 persist,则会发生以下两种情况之一,如果 persist 操作是级联的,则 A 也将保存到数据库中。如果未级联持久化,您将收到错误,因为托管实体引用了非托管实体,这在 EclipseLink 上给出了以下错误:“在同步期间,通过未标记为级联 PERSIST 的关系发现了新对象”。级联持久化是有意义的,因为您经常同时创建父实体及其子实体。

如果要从另一个实体 B 上的集合中删除实体 A,则不能依赖级联,因为您不是在删除 B。相反,您必须直接在 A 上调用 remove,将其从 B 上的集合中删除不会产生任何影响,因为尚未在 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 实体的外键,这意味着重要的是 and,因为您必须设置所有对外关系,并保留任何新的 。同样,从 Header 中删除现有详细信息与 Header 无关,定义关系(外键)位于 Details 中,因此从持久性上下文中删除详细信息是将其从数据库中删除的原因。您也可以使用 orphanRemoval,但这需要为每个事务添加额外的逻辑,并且在我看来,如果每个 peristence 操作都是显式的,则代码更易于阅读,这样您就不需要返回实体来读取注释。detail.setHeader(header);em.persist(details)Details

最后:代码中的持久性操作序列不会转换为对数据库执行的查询的顺序。Hibernate 和 EclipseLink 都将首先插入新实体,然后删除现有实体。根据我的经验,这是“主键已经存在”的最常见原因。如果删除具有特定主键的实体,然后添加具有相同主键的新实体,则插入将首先发生,并导致键冲突。可以通过告诉 JPA 将当前持久性状态刷新到数据库来解决此问题。 会将删除查询推送到数据库,以便您可以插入与已删除行具有相同主键的另一行。em.flush()

那是很多信息,如果有什么你不理解,或者需要我澄清,请让我知道。


答案 2

原因由@klaus-groenbaek描述,但我在解决它时注意到了一些有趣的事情。

在使用Spring时,我无法在使用派生方法时使其正常工作。JpaRepository

因此,以下内容不起作用:

void deleteByChannelId(Long channelId);

但是指定显式 () 可以使其正常工作,因此以下方法有效:ModifyingQuery

@Modifying
@Query("delete from ClientConfigValue v where v.channelId = :channelId")
void deleteByChannelId(@Param("channelId") Long channelId);

在这种情况下,语句以正确的顺序提交/持久化。


推荐