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

00jrzges  于 2023-05-21  发布在  Java
关注(0)|答案(4)|浏览(314)

所以,我有这样一个场景,我需要获取一个头记录,删除它的细节,然后以不同的方式重新创建细节。更新细节太麻烦了。
我基本上有:

@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详细信息列表。
但现在的情况是,在创建之前,删除似乎并不受欢迎。因为错误是重复键约束。重复的密钥是上面的“无效”场景。
如果我用一堆细节手动填充数据库,并注解掉CREATE details部分(只运行delete),那么记录就被删除了。所以删除起作用了。作品作品。只是两者不能同时工作。
我可以提供更多的代码是需要的。我用的是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
事实上,我最初没有提到这一点,但我第一次是这样做的。另外,我使用级联。所有我假设包括持久性。
为了测试,我将代码更新为:

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

再次...我想重申....如果我没有立即创建,删除将起作用。创建将工作,如果我没有立即删除它之前。但如果它们在一起,则两者都不起作用,因为数据库中存在重复键约束错误。
我已经尝试了相同的场景,有和没有级联删除。

eaf3rand

eaf3rand1#

请注意,这是一个相当长的解释,但是当我查看您的代码时,您似乎遗漏了一些关于JPA如何工作的关键概念。
首先,向集合中添加实体或从集合中删除实体并不意味着数据库中将发生相同的操作,除非使用级联或orphanRemoval传播持久化操作。
对于要添加到数据库中的实体,必须直接调用EntityManager.persist(),或者通过级联persist调用。这基本上就是在JPARepository.save()中发生的事情
如果希望删除实体,则必须直接调用EntityManager.remove(),或者通过级联操作,或者通过JpaRepository.delete()
如果您有一个托管实体(加载到持久性上下文中的实体),并且您修改了事务中的基本字段(非实体、非集合),那么在事务提交时,即使您没有调用persist/save,也会将此更改写入数据库。持久化上下文保持每个加载实体的内部副本,并且当事务提交时,它循环通过内部副本并与当前状态进行比较,并且任何基本字段更改都会触发更新查询。
如果你已经添加了一个新的实体(A)到另一个实体(B)的集合中,但是没有对A调用persist,那么A将不会被保存到数据库中。如果你在B上调用persist,会发生以下两种情况之一:如果persist操作是级联的,A也会被保存到数据库中。如果persist没有级联,你会得到一个错误,因为一个托管实体引用了一个非托管实体,这在EclipseLink上给予了这个错误:“在同步过程中,通过未标记为级联PERSIST的关系找到了新对象”。级联持久化是有意义的,因为您经常同时创建父实体及其子实体。
当你想从另一个实体B的集合中删除一个实体A时,你不能依赖级联,因为你不是在删除B。相反,您必须直接在A上调用remove,从B上的集合中删除它不会有任何效果,因为在EntityManager上没有调用持久化操作。你也可以使用orphanRemoval来触发delete,但是我建议你在使用这个特性时要小心,特别是因为你似乎缺少一些关于持久化操作如何工作的基本知识。
通常,考虑持久化操作以及它必须应用于哪个实体是有帮助的。下面是如果我写了代码的样子。

@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()将把删除查询推送到数据库,这样你就可以插入另一行,它的主键与你删除的行的主键相同。
这是一个很大的信息,请让我知道,如果有什么你不明白,或需要我澄清。

dkqlctbz

dkqlctbz2#

原因是由@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);

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

gcuhipw9

gcuhipw93#

首先,只执行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:是的,删除这个实体,我已经从集合中删除了,然后添加新的实体。

dojqjjoe

dojqjjoe4#

也许em.flush()就可以了。
所以代码可以是:

@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);
    }
    detailRepository.flush();

    // 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)
    }
}
`

相关问题