Быстрое решение (но не рекомендуется)
Ошибка 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"
.
Итак, что мы делаем, так это инкапсулируем в Category.addReport(...)
всю логику перекрестных ссылок между Category
и Report
.
Учитывая приведенную выше версию метода 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
за ошибку, описанную в вашем вопросе, многие проблемы могут возникнуть при использовании Lombok вместе с Hibernate:
Свойства загружаются, даже если помечены для отложенной загрузки...
При генерации hashcode()
, equals()
или toString()
с использованием Lombok весьма вероятно, что будут вызваны геттеры полей, помеченных как ленивые. Таким образом, первоначальное намерение программиста отложить загрузку некоторых свойств не будет учтено, поскольку они будут извлечены из базы данных при вызове одного из hascode(), equals() или toString().
В лучшем случае, если сессия открыта, это вызовет дополнительные запросы и замедлит ваше приложение.
В худшем случае, когда сеанс недоступен, будет выдано исключение LazyInitializationException.
Хэш-код()/equals() Ломбока влияет на поведение collections
Hibernate использует логику hascode() и equals() для проверки порядка объектов, чтобы избежать повторной вставки одного и того же объекта. То же самое относится и к удалению из списка.
То, как Lombok генерирует методы hashcode() и equals(), может повлиять на спящий режим и создать несогласованные свойства (особенно коллекции).
Дополнительную информацию по этому вопросу см. в этой статье: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/
Кратко об интеграции Lombok/Hibernate
Не используйте Ломбок для занятий entity
. Аннотации Lombok, которых вам следует избегать, — это @Data
, @ToString
и @EqualsAndHashCode
.
Не по теме. Остерегайтесь удаления-сироты
В Category
отображение @OneToMany
определяется с помощью orphanRemoval=true
, как показано ниже:
@OneToMany(mappedBy = "category", cascade = CascadeType.ALL, orphanRemoval = true)
private Collection<Report> reports;
orphanRemoval=true
означает, что при удалении категории все отчеты в этой категории также будут удалены.
Важно оценить, является ли это желаемым поведением в вашем приложении.
См. пример спящего режима SQL, который будет выполняться при вызове 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=?
person
francisco neto
schedule
26.10.2020