На коллекцию с cascade=all-delete-orphan больше не ссылался экземпляр объекта-владельца — Spring и Lombok

Я получаю эту ошибку A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance с моим отношением oneToMany при попытке обновить дочерний элемент (отчет). Хотя я вижу, что этот вопрос задавался здесь несколько раз, я не смог заставить свой код работать с ними, и теперь я чувствую, что это может быть проблемой, когда я использую Ломбок, возможно, поскольку большинство ответов здесь упоминают об изменениях на методы 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;
    }

}

Спасибо большое.


person Francislainy Campos    schedule 26.10.2020    source источник
comment
Вы можете добавить свои реализации equals и hashcode даже при использовании lombok... если вы предоставите реализацию, Lombok отложит это вам... так что, если вы считаете, что equals и hashcode могут быть источником проблемы, просто добавьте свои предпочтительные реализации.   -  person James Gawron    schedule 27.10.2020


Ответы (2)


Быстрое решение (но не рекомендуется)

Ошибка 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
comment
Привет, Франциско, большое спасибо за такой отличный и подробный ответ. Я бы проголосовал за вас десять раз, если бы мог. Это великолепно, и это решает мою проблему. Большое спасибо! - person Francislainy Campos; 27.10.2020
comment
Единственное, что я должен отметить, это то, что как более простые, так и самые подробные ответы дают мне ошибку переполнения стека. Кажется, это связано с круговыми отношениями, когда я проверяю завиток на предмет ответа, который теперь действительно огромен, с отчетами внутри категорий, которые уже имеют этот отчет в качестве своего дочернего элемента, который снова имеет эту категорию в качестве своего дочернего элемента, и т. д. Я пытаюсь чтобы выяснить это сейчас, так как, несмотря на эту ошибку, мое обновление проходит. Это коммит с более простой версией. github.com/francislainy/gatling_tool_backend/commit/ - person Francislainy Campos; 27.10.2020
comment
А вот коммит с самым развернутым ответом github.com/francislainy/gatling_tool_backend/commit/ - person Francislainy Campos; 27.10.2020
comment
Это может быть еще одним побочным эффектом ломбока, поскольку он может быть вызван автоматически сгенерированным getReports(). Если это так, есть несколько решений, которые вы можете попробовать: не генерировать геттер или аннотировать стороны отношения с помощью @JsonManagedReference и @JsonBackReference. См. здесь перед примерами: вызывает бесконечный цикл или пустые записи в json"> stackoverflow.com/questions/16577907/ - person francisco neto; 27.10.2020
comment
Спасибо. Боюсь, я пытался удалить Lombok и добавить эти аннотации @json, но та же проблема все еще возникает. - person Francislainy Campos; 27.10.2020
comment
Теперь это работает. :) Не нуждался в дополнительных аннотациях и смог сохранить Lombok как есть, но должен был создать новый объект категории, в котором не было коллекции отчетов, вместо того, чтобы использовать существующую категорию. - person Francislainy Campos; 30.10.2020

Просто обновление, основанное на принятом ответе, чтобы избежать StackOverflow и циклического цикла, который возник после изменений.

Мне пришлось создать новый объект категории, чтобы удалить отчеты внутри него в моем возвращаемом 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);
    }
}

}
person Francislainy Campos    schedule 30.10.2020