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

A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance - Spring and Lombok

尝试更新我的子元素(报告)时,我的 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;
    }

}

非常感谢。

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

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

重要的是要注意将类别绑定到报表和 vice-versa 不是由 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之间cross-reference的所有逻辑封装在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() 的方式可能会影响休眠并创建不一致的属性(尤其是集合)。

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

Lombok/Hibernate 整合简而言之

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

Off-topic - 提防delete-orphan

Category中,@OneToMany映射定义为orphanRemoval=true如下:

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

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

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

查看调用 categoryRepository.delete(category):

时 hibernate 将执行的 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=?

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

我必须创建一个新的类别对象以删除我的 return 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);
    }
}

}