拥有实体实例 - Spring 和 Lombok 不再引用具有 cascade="all-delete-orphan" 的集合

Posted

技术标签:

【中文标题】拥有实体实例 - Spring 和 Lombok 不再引用具有 cascade="all-delete-orphan" 的集合【英文标题】:A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance - Spring and Lombok 【发布时间】:2020-10-26 03:06:31 【问题描述】:

尝试更新我的子元素(报告)时,我的 oneToMany 关系出现此 A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance 错误。虽然我在这里看到这个问题被问了几次,但我无法让我的代码与他们一起工作,我现在觉得这可能是我使用 Lombok 的问题,因为这里的大多数答案都提到了关于Lombok 抽象出来的 hashcode 和 equals 方法?我试图删除 Lombok 以尝试不使用它,但后来我对下一步该怎么做感到有些困惑。如果我能得到一些关于如何在我原来的 Lombok 实现中解决这个问题的指导。

@Entity
@Table(name = "category")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Category 

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Column(name = "category_title", nullable = false)
private String title;

@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> report;

public Category(UUID id, String title) 

    this.id = id;
    this.title = title;




@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "report")
@Data
public class Report 

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Column(name = "report_title", nullable = false)
private String reportTitle;

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
@JoinColumn(name = "category_id",  nullable = false)
private Category category;

public Report(UUID id) 
    this.id = id;




 @Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) 

    if (reportRepository.findById(id).isPresent()) 

        Report existingReport = reportRepository.findById(id).get();
        existingReport.setReportTitle(reportUpdateDto.getTitle());

        Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
        Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
        existingReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties


        Report updatedReport = reportRepository.save(existingReport);
        updatedReport.setCategory(category); // This is needed to remove hibernate interceptor to be set together with the other category properties


        ReportUpdateDto newReportUpdateDto = new ReportUpdateDto(updatedReport.getId(),
                updatedReport.getReportTitle(), updatedReport.getCategory());


        return newReportUpdateDto;

     else 
        return null;
    


非常感谢。

【问题讨论】:

即使在使用 lombok 时,您也可以添加您的 equals 和 hashcode 实现...如果您提供实现 Lombok 将遵从您...所以如果您认为 equals 和 hashcode 可能是问题的根源,只需放入您喜欢的实现即可。 【参考方案1】:

快速解决方案(但不推荐)

collection [...] no longer referenced 的错误出现在您的代码中,因为双向映射 category-report 的两侧之间的同步只是部分完成。

请务必注意,将类别绑定到报告(反之亦然)不是由 Hibernate 完成的。我们必须自己做这个,在代码中,为了同步双方的关系,否则我们可能会破坏领域模型关系的一致性。

在您的代码中,您已经完成了一半的同步(将类别绑定到报告):

existingReport.setCategory(category);

缺少的是报告与类别的绑定:

category.addReport(existingReport);

Category.addReport() 可能是这样的:

public void addReport(Report r)
    if (this.report == null)
        this.report = new ArrayList<>();
    
    this.report.add(r);

推荐解决方案 - 同步映射两侧的最佳实践

上面建议的代码可行,但很容易出错,因为程序员在更新关系时可能会忘记调用其中一行。

更好的方法是将同步逻辑封装在关系的拥有方的方法中。另一面是Category,如下所述:mappedBy = "category"

所以我们要做的就是将CategoryReport之间的所有交叉引用逻辑封装在Category.addReport(...)中。

考虑到上面版本的addReport()方法,缺少的是添加r.setCategory(this)

public class Category 


    public void addReport(Report r)
        if (this.reports == null)
            this.reports = new ArrayList<>();
        
        r.setCategory(this);
        this.reports.add(r);
    

现在,在updateReport() 中调用addReport() 就足够了,下面的注释行可以删除:

//existingReport.setCategory(category); //That line can be removed
category.addReport(existingReport);

Category 中也包含removeReport() 方法是一个很好的做法:

public void removeReport(Report r)
    if (this.reports != null)
        r.setCategory = null;
        this.reports.remove(r);
    

也就是Category.java这两个方法添加后的代码:

public class Category 


    @OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
    private Collection<Report> reports;
    

    //Code ommited for brevity
    
    
    public void addReport(Report r)
        if (this.reports == null)
            this.reports = new ArrayList<>();
        
        r.setCategory(this);
        this.reports.add(r);
    
    
    public void removeReport(Report r)
        if (this.reports != null)
            r.setCategory = null;
            this.reports.remove(r);
        
    

现在更新报告类别的代码是这样的:

public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) 

    if (reportRepository.findById(id).isPresent()) 

        Report existingReport = reportRepository.findById(id).get();
        existingReport.setReportTitle(reportUpdateDto.getTitle());

        Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();
        existingCategory.addReport(existingReport);
        reportRepository.save(existingReport);

        return new ReportUpdateDto(existingReport.getId(),
                existingReport.getReportTitle(), existingReport.getCategory());
     else 
        return null;
    

查看双向关联中同步的实际示例的好资源:https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/

Lombok 和 Hibernate - 不是最好的组合

虽然我们不能将您问题中描述的错误归咎于Lombok,但在将 Lombok 与 Hibernate 一起使用时可能会出现许多问题:

即使标记为延迟加载,属性也会被加载...

当使用 Lombok 生成 hashcode()equals()toString() 时,标记为惰性的字段的 getter 很可能会被调用。因此,程序员推迟某些属性加载的最初意图将不会得到尊重,因为它们将在调用 hascode()、equals() 或 toString() 之一时从数据库中检索。

在最好的情况下,如果会话打开,这将导致额外的查询并减慢您的应用程序。

在最坏的情况下,当没有可用的会话时,将抛出 LazyInitializationException。

Lombok 的 hashcode()/equals() 影响 collections 的行为

Hibernate 使用 hascode() 和 equals() 逻辑来检查一个对象是否是为了避免插入同一个对象两次。这同样适用于从列表中删除。

Lombok 生成方法 hashcode() 和 equals() 的方式可能会影响休眠并创建不一致的属性(尤其是 Collections)。

有关此主题的更多信息,请参阅本文:https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/

Lombok/Hibernate 集成简介

不要将 Lombok 用于 entity 类。您需要避免的 Lombok 注释是 @Data@ToString@EqualsAndHashCode

离题 - 谨防删除孤儿

Category 中,@OneToMany 映射是用orphanRemoval=true 定义的,如下所示:

@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;

orphanRemoval=true 表示当删除一个类别时,该类别中的所有报告也将被删除。

评估这是否是您的应用程序中所需的行为很重要。

查看调用categoryRepository.delete(category)时休眠将执行的SQL示例:

    //Retrieving all the reports associated to the category
    select
        report0_.category_id as category3_1_0_,
        report0_.id as id1_1_0_,
        report0_.id as id1_1_1_,
        report0_.category_id as category3_1_1_,
        report0_.report_title as report_t2_1_1_ 
    from
        report report0_ 
    where
        report0_.category_id=?
    //Deleting all the report associated to the category (retrieved in previous select)
    delete from
            report 
        where
            id=?
    //Deleting the category
    delete from
            category 
        where
            id=?

【讨论】:

您好,弗朗西斯科,非常感谢您提供如此出色而详细的回答。如果可以的话,我会给你十次投票。这太棒了,它解决了我的问题。非常感谢! 唯一我必须指出的是,更简单和最广泛的答案都会给我一个堆栈溢出错误。似乎是由于循环关系,因为当我检查 curl 的响应时,它现在真的很大,在已经将该报告作为其子级的类别内的报告再次将该类别作为其子级,等等。我正在尝试现在要弄清楚这一点,因为尽管出现了这个错误,但我的更新仍在进行中。这是更简单版本的提交。 github.com/francislainy/gatling_tool_backend/commit/… 这里是答案最广泛的提交github.com/francislainy/gatling_tool_backend/commit/… 这可能是 lombok 的另一个副作用,因为它可能是由自动生成的 getReports() 引起的。如果是这种情况,您可以尝试一些解决方案,例如不生成 getter,或者用@JsonManagedReference@JsonBackReference 注释关系的两侧。请参阅此处的示例:***.com/questions/16577907/… 它正在工作。 :) 不需要额外的注释并且能够保持 Lombok 原样,但必须创建一个新的 Category 对象,其中没有报告集合,而不是使用 existingCategory。【参考方案2】:

只是根据接受的答案进行更新,以避免在更改后出现 *** 和循环。

我必须创建一个新的 Category 对象以在我的返回 dto 中删除其中的报告,否则由于该类别包含相同的报告,它再次包含该类别等等,我的响应中可以看到无限循环。

@Override
public ReportUpdateDto updateReport(UUID id, ReportUpdateDto reportUpdateDto) 


    if (reportRepository.findById(id).isPresent()) 

        Report existingReport = reportRepository.findById(id).get();
        existingReport.setReportTitle(reportUpdateDto.getTitle());

        Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();

        Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
        existingCategory.addReport(existingReport);

        reportRepository.save(existingReport);

        return new ReportUpdateDto(existingReport.getId(),
                existingReport.getReportTitle(), existingReport.getRun_date(),
                existingReport.getCreated_date(), category);

     else 
        return null;
    


所以添加了这部分:

Category existingCategory = categoryRepository.findById(reportUpdateDto.getCategory().getId()).get();

Category category = new Category(existingCategory.getId(), existingCategory.getTitle());
existingCategory.addReport(existingReport);

好像我有类似的东西

Category category = new Category(existingCategory.getId(), existingCategory.getTitle(), existingCategory.getReports);

我可以再次看到问题,这就是 existingCategory 对象本身包含的内容。

这里是我的最终实体

@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "report")
@Data
public class Report 

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Column(name = "report_title", nullable = false)
private String reportTitle;


@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.MERGE)
@JoinColumn(name = "category_id", nullable = false)
private Category category;


@Entity
@Table(name = "category")
@AllArgsConstructor
@NoArgsConstructor
@Data
public class Category 

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
@Column(name = "category_title", nullable = false)
private String title;

@OneToMany(fetch = FetchType.LAZY, mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;

public Category(UUID id, String title) 

    this.id = id;
    this.title = title;


public void addReport(Report r) 
    if (this.reports == null) 
        this.reports = new ArrayList<>();
    
    r.setCategory(this);
    this.reports.add(r);


public void removeReport(Report r) 
    if (this.reports != null) 
        r.setCategory(null);
        this.reports.remove(r);
    



【讨论】:

以上是关于拥有实体实例 - Spring 和 Lombok 不再引用具有 cascade="all-delete-orphan" 的集合的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot Rest API 返回与 Lombok 一起使用的空 JSON

lombok的使用

Spring Boot打jar包,排除lombok等scope=provided的依赖

Android开发插件---Lombok

Spring Data JPA 多对多表查询

记录一些遇见的bug——mapstruct和lombok同时使用时,转换实体类时数据丢失问题