【问题标题】:Delete then create records are causing a duplicate key violation with Spring Data JPA删除然后创建记录导致 Spring Data JPA 重复键冲突
【发布时间】:2017-06-26 17:09:21
【问题描述】:

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

我基本上有:

@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

@克劳斯·格罗恩贝克

其实,我一开始并没有提到这一点,但我第一次是这样说的。另外,我正在使用 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)
    }
}

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

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

【问题讨论】:

  • 这取决于您如何映射详细信息...只是简单地.remove(...) 他们并不总是删除数据库中的行
  • 所以你必须为你的实体提供注释
  • 不确定为什么我的票数接近?无论如何...... @Andremoniy 是的,我已经用 Cascade 注释了我的实体。我将使用该信息更新问题。
  • 是我,我撤回了我的投票
  • 我没有意识到插入总是发生在删除之前,即使您先执行删除也是如此。在每个进程之间刷新 EM 就可以了。

标签: java spring jpa persistence


【解决方案1】:

首先,只执行header.getDetails().remove(detail); 不对DB 执行任何类型的操作。我想在headerService.save(header); 中你调用了类似session.saveOrUpdate(header) 的东西。

基本上这是某种逻辑冲突,因为 Hibernate 需要在一个操作中删除和创建具有重复键的实体,但 它不知道应该执行这些操作的顺序

我建议至少在添加新细节之前调用headerService.save(header);,例如:

    // 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:是的,删除我已从集合中删除的这个实体,然后添加新实体。

【讨论】:

    【解决方案2】:

    请注意,因为这是一个相当长的解释,但是当我查看您的代码时,您似乎遗漏了有关 JPA 工作原理的几个关键概念。

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

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

    如果你想删除一个实体,你必须直接调用EntityManager.remove(),或者通过级联操作,或者通过JpaRepository.delete()

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

    如果您已将新实体 (A) 添加到另一个实体 (B) 上的集合中,但尚未在 A 上调用持久化,则 A 将不会保存到数据库中。如果在 B 上调用 persist 会发生两种情况之一,如果将 persist 操作级联,则 A 也将被保存到数据库中。如果persist 没有级联,您将收到错误,因为托管实体引用非托管实体,这会在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实体的外键,这意味着重要的是detail.setHeader(header);em.persist(details),因为您必须设置所有外关系,并保留任何新的Details。 同样,从 Header 中删除现有详细信息与 Header 无关,定义关系(外键)在 Details 中,因此从持久性上下文中删除详细信息是将其从数据库中删除。您也可以使用 orphanRemoval,但这需要为每个事务添加额外的逻辑,并且在我看来,如果每个持久性操作都是显式的,则代码更易于阅读,这样您就不需要返回实体来阅读注释。

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

    这是很多信息,如果您有任何不明白的地方,或者需要我澄清,请告诉我。

    【讨论】:

    • “Hibernate 和 EclipseLink 都将首先插入新实体,然后删除现有实体” 对我来说很关键,在 DELETEINSERT 之间刷新帮助(y )。
    【解决方案3】:

    @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);
    

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

    【讨论】:

    • 不幸的是,这是它唯一的工作方式。 CrudRepository 应该是开箱即用的。我们不会重新创建具有相同主键的,但对某些列有唯一约束。在删除现有约束值后立即插入相同的唯一约束值需要相同的解决方法。 @Martin:非常感谢您的解决方法。
    • 感谢@martin...我也在苦苦挣扎,这个解决方案对我有用。但原因是什么,为什么它以这种方式正常工作。
    • 好吧,这里的问题是,@Query 如果你不写它很重要,那么它不会与唯一的“修改”一起使用。这肯定是一个错误。我也在 GitHub 上看到了一个关于这个的线程
    猜你喜欢
    • 2015-01-24
    • 2012-12-20
    • 2016-12-07
    • 1970-01-01
    • 2017-06-20
    • 2012-08-05
    • 2017-04-04
    • 2020-12-15
    • 1970-01-01
    相关资源
    最近更新 更多