具有 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"
.
所以我们要做的就是把Category
和Report
之间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);
}
}
}
尝试更新我的子元素(报告)时,我的 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"
.
所以我们要做的就是把Category
和Report
之间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)
:
//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);
}
}
}