删除然后创建记录会导致 Spring Data JPA 出现重复键冲突

2024-05-01

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

我基本上有:

@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.

Thanks

UPDATE

我的实体基本上注释如下:

@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;
...
}

UPDATE 2

@克劳斯·格伦贝克

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

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

我尝试过使用和不使用级联删除的相同场景。


请耐心等待,因为这是一个相当长的解释,但是当我查看您的代码时,您似乎缺少一些有关 JPA 如何工作的关键概念。

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

对于要添加到数据库的实体,您必须调用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 上调用删除,从 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 恰好是 Details 实体的外键,这意味着重要的是detail.setHeader(header); and em.persist(details),因为你必须设置所有的外交关系,并坚持任何新的Details。 同样,从标头中删除现有详细信息与标头无关,定义关系(外键)位于详细信息中,因此从持久性上下文中删除详细信息就是将其从数据库中删除。您还可以使用 orphanRemoval,但这需要为每个事务添加额外的逻辑,并且在我看来,如果每个持久性操作都是显式的,则代码更容易阅读,这样您就不需要返回实体来阅读注释。

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

信息量很大,如果有什么不明白或者需要我澄清的地方请告诉我。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

删除然后创建记录会导致 Spring Data JPA 出现重复键冲突 的相关文章

随机推荐