1

I am getting this A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance error with my oneToMany relationship when trying to update my child element (report). Although I see this question asked a few times here, I haven't been able to make my code to work with them and I now feel it may be an issue with me using Lombok perhaps, since most of the answers here mention about changes on the hashcode and equals methods, which are abstracted away by Lombok? I tried to remove Lombok to try without it but then I got a bit confused on what to do next. If I could get some guidance on how to fix this issue within my original Lombok implementation please.

@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;
    }

}

Thank you very much.

Francislainy Campos
  • 3,462
  • 4
  • 33
  • 81
  • You can add your equals and hashcode implementations even while using lombok ... if you provide an implementation Lombok will defer to you ... so if you think equals and hashcode may be the source of the problem, just drop in your preferred implementations. – James Gawron Oct 26 '20 at 21:35

2 Answers2

2

Fast solution (but not recommended)

The error of collection [...] no longer referenced arrises in your code beacuse the synchronization between both sides of the bidiretional mapping category-report was just partially done.

It's important to note that binding the category to the report and vice-versa is not done by Hibernate. We must do this ouserselves, in the code, in order to sync both sides of the relationship, otherwise we may break the Domain Model relationship consistency.

In your code you have done half of the synchronization (binding the category to the report):

existingReport.setCategory(category);

What is missing is the binding of the report to the category:

category.addReport(existingReport);

where the Category.addReport() may be like that:

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

Recommended Solution - Best practice for synchronizing both sides of the mapping

The suggested code above works, but it is error prone as the programmer may forget to call one of the lines when updating the relationship.

A better approach is to encapsulate that sychronization logic in a method in the owning side of the relationship. And that side is the Category as stated here: mappedBy = "category".

So what we do is to encapsulate in the Category.addReport(...) all the logic of cross-reference between Category and Report.

Considering the above version of addReport() method, what is missing is adding 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);
    }
}

Now, in the updateReport() it is enough to call the addReport() and the commented line bellow can be deleted:

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

It is a good practice including in Category a removeReport() method as well:

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

That is the code of Category.java after the two methods were added:

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);
        }
    }
}

And the code for updating a report category now is this:

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;
    }
}

A good resource to see a practical example of synchronization in bidirectional associations: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/

Lombok and Hibernate - not the best of the combinations

Though we can not blame Lombok for the error described in your question, many problems may arrise when using Lombok alongside with Hibernate:

Properties being loaded even if marked for lazy loading...

When generating hashcode(), equals() or toString() using Lombok, the getters of fields marked as lazy are very likelly to be called. So the programmer's initial intention of postponing some properties loading will no be respected as they will be retrieved from the database when one of hascode(), equals() or toString() is invoked.

In the best case scenario, if a session is open, this will cause additional queries and slow down your application.

In the worst case scenarios, when no session is available, a LazyInitializationException will be thrown.

Lombok's hashcode()/equals() affecting the bevahior of collections

Hibernate uses hascode() and equals() logic to check if a object is order to avoid inserting the same object twice. The same applies to removing from a list.

The way Lombok generates the methods hashcode() and equals() may affect hibernate and create inconsistent properties (especially Collections).

See this article for more info on this subject: https://thorben-janssen.com/lombok-hibernate-how-to-avoid-common-pitfalls/

Lombok/Hibernate integration in a nutshell

Don't use Lombok for entity classes. Lombok annotations you need to avoid are @Data, @ToString, and @EqualsAndHashCode.

Off-topic - Beware of delete-orphan

In Category, the @OneToMany mapping is defined with orphanRemoval=true as bellow:

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

The orphanRemoval=true means that when deleting a category, all the reports in that category will be deleted as well.

It is important to assess if that is the desired behavior in your application.

See an example of the SQLs hibernate will execute when calling 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=?
francisco neto
  • 797
  • 1
  • 5
  • 13
  • Hi Francisco, thanks so so much for such a great and detailed answered. I would upvote it you ten times if I could. This is brilliant and it fixes my issue. Thanks a lot! – Francislainy Campos Oct 27 '20 at 02:58
  • Only thing I must point out is that both the simpler and most extensive answers give me a stack overflow error. It seems due to a circular relationship as when I inspect the curl for the response it's really huge now, with reports inside categories that already have that report as their child again which has that category once more as their child, etc. I'm trying to figure this out now, as despite this error my update is going through. This is the commit with the simpler version. https://github.com/francislainy/gatling_tool_backend/commit/6674a3e5d327a851deec03400fc6dd0b29429d84 – Francislainy Campos Oct 27 '20 at 03:32
  • And here the commit with the most extensive answer https://github.com/francislainy/gatling_tool_backend/commit/bec3ac53d8fa1ab634dad6db0aa7fff4fab8c186 – Francislainy Campos Oct 27 '20 at 03:43
  • This can be another side effect of lombok as it may be caused by the auto generated getReports(). If that's the case there are a few solutions you may try as not generating the getter, or anotating the sides of the relationship with `@JsonManagedReference` and `@JsonBackReference`. See here fore examples: https://stackoverflow.com/questions/16577907/hibernate-onetomany-relationship-causes-infinite-loop-or-empty-entries-in-json – francisco neto Oct 27 '20 at 10:05
  • Thank you. I'm afraid I tried removing Lombok and adding these @json annotations but the same issue still happens. – Francislainy Campos Oct 27 '20 at 15:23
  • 1
    It's working now. :) Didn't need the extra annotations and was able to keep Lombok as it was but had to create a new Category object that didn't have the reports collection inside it instead of going with existingCategory. – Francislainy Campos Oct 30 '20 at 03:54
0

Just an update based on the accepted answer to avoid a StackOverflow and circular loop that came up after the changes.

I had to create a new Category object to remove the reports inside it within my return dto, otherwise as the category contains that same report, that again contains that category and so on, the infinite loop could be seen on my response.

@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;
    }

}

So added this part:

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

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

As if I have something like

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

I can see the issue once again, which is what the existingCategory object itself contains.

And here my final entities

@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);
    }
}

}
Francislainy Campos
  • 3,462
  • 4
  • 33
  • 81