Difficult with CASCADE and orphanRemoval

And now I have a new difficult. =(

I’m trying to use CASCADE for delete Page’s when a Issue obj is deleted. I also want to use orphanRemoval. Nothing is working. Page’s are never removed.

Entities classes sources:

Issue:


import javax.persistence.*;
import java.util.Date;

import com.haulmont.cuba.core.entity.StandardEntity;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.chile.core.annotations.Composition;
import com.haulmont.cuba.core.entity.annotation.OnDelete;
import com.haulmont.cuba.core.global.DeletePolicy;
import java.util.List;
import com.haulmont.cuba.core.entity.BaseUuidEntity;
import com.haulmont.cuba.core.entity.Versioned;
import com.haulmont.cuba.core.entity.Updatable;
import com.haulmont.cuba.core.entity.SoftDelete;

/**
 * @author arimolo
 */
@NamePattern("%s %s|issueNumber,issueDate")
@Table(name = "MAGAZINEAPPBACKEND_ISSUE")
@Entity(name = "magazineappbackend$Issue")
public class Issue extends BaseUuidEntity implements Versioned, Updatable, SoftDelete {
    private static final long serialVersionUID = 6829775742991251725L;

    @Column(name = "ISSUE_NUMBER", nullable = false)
    protected Integer issueNumber;

    @Temporal(TemporalType.DATE)
    @Column(name = "ISSUE_DATE", nullable = false)
    protected Date issueDate;

    @OrderBy("pageNumber")
    @OnDelete(DeletePolicy.CASCADE)
    @OneToMany(mappedBy = "issue", cascade = CascadeType.ALL, orphanRemoval = true)
    protected List<Page> pages;

    @Column(name = "COVER")
    protected String cover;

    @Version
    @Column(name = "VERSION", nullable = false)
    protected Integer version;

    @Column(name = "UPDATE_TS")
    protected Date updateTs;

    @Column(name = "UPDATED_BY", length = 50)
    protected String updatedBy;

    @Column(name = "DELETE_TS")
    protected Date deleteTs;

    @Column(name = "DELETED_BY", length = 50)
    protected String deletedBy;

    @Override
    public Boolean isDeleted() {
        return deleteTs != null;
    }

    @Override
    public void setDeletedBy(String deletedBy) {
        this.deletedBy = deletedBy;
    }

    @Override
    public String getDeletedBy() {
        return deletedBy;
    }

    @Override
    public void setDeleteTs(Date deleteTs) {
        this.deleteTs = deleteTs;
    }

    @Override
    public Date getDeleteTs() {
        return deleteTs;
    }


    @Override
    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

    @Override
    public String getUpdatedBy() {
        return updatedBy;
    }

    @Override
    public void setUpdateTs(Date updateTs) {
        this.updateTs = updateTs;
    }

    @Override
    public Date getUpdateTs() {
        return updateTs;
    }


    @Override
    public void setVersion(Integer version) {
        this.version = version;
    }

    @Override
    public Integer getVersion() {
        return version;
    }


    public void setCover(String cover) {
        this.cover = cover;
    }

    public String getCover() {
        return cover;
    }


    public void setPages(List<Page> pages) {
        this.pages = pages;
    }

    public List<Page> getPages() {
        return pages;
    }


    public void setIssueNumber(Integer issueNumber) {
        this.issueNumber = issueNumber;
    }

    public Integer getIssueNumber() {
        return issueNumber;
    }

    public void setIssueDate(Date issueDate) {
        this.issueDate = issueDate;
    }

    public Date getIssueDate() {
        return issueDate;
    }


}

Page:


package net.gocoders.magazineappbackend.entity;

import javax.persistence.Entity;
import javax.persistence.Table;
import javax.persistence.Column;
import javax.persistence.Lob;
import com.haulmont.cuba.core.entity.StandardEntity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import com.haulmont.chile.core.annotations.NamePattern;
import com.haulmont.cuba.core.entity.BaseUuidEntity;
import com.haulmont.cuba.core.entity.Versioned;
import javax.persistence.Version;
import com.haulmont.cuba.core.entity.Updatable;
import java.util.Date;

/**
 * @author arimolo
 */
@NamePattern("%s %s|issue,pageNumber")
@Table(name = "MAGAZINEAPPBACKEND_PAGE")
@Entity(name = "magazineappbackend$Page")
public class Page extends BaseUuidEntity implements Versioned, Updatable {
    private static final long serialVersionUID = -8760048507768183964L;

    @Column(name = "PAGE_NUMBER", nullable = false)
    protected Integer pageNumber;

    @Lob
    @Column(name = "HTML_CONTENT")
    protected String htmlContent;

    @Lob
    @Column(name = "TEXT_CONTENT")
    protected String textContent;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "ISSUE_ID")
    protected Issue issue;

    @Version
    @Column(name = "VERSION", nullable = false)
    protected Integer version;

    @Column(name = "UPDATE_TS")
    protected Date updateTs;

    @Column(name = "UPDATED_BY", length = 50)
    protected String updatedBy;

    @Override
    public void setUpdatedBy(String updatedBy) {
        this.updatedBy = updatedBy;
    }

    @Override
    public String getUpdatedBy() {
        return updatedBy;
    }

    @Override
    public void setUpdateTs(Date updateTs) {
        this.updateTs = updateTs;
    }

    @Override
    public Date getUpdateTs() {
        return updateTs;
    }


    @Override
    public void setVersion(Integer version) {
        this.version = version;
    }

    @Override
    public Integer getVersion() {
        return version;
    }


    public void setIssue(Issue issue) {
        this.issue = issue;
    }

    public Issue getIssue() {
        return issue;
    }


    public void setPageNumber(Integer pageNumber) {
        this.pageNumber = pageNumber;
    }

    public Integer getPageNumber() {
        return pageNumber;
    }

    public void setHtmlContent(String htmlContent) {
        this.htmlContent = htmlContent;
    }

    public String getHtmlContent() {
        return htmlContent;
    }

    public void setTextContent(String textContent) {
        this.textContent = textContent;
    }

    public String getTextContent() {
        return textContent;
    }



Thank you a lot! =)

Hi Alexandre,

First of all, do not use CASCADE on JPA level (in @OneToMany annotation) - it is not needed in CUBA and can cause issues.
The @OnDelete(DeletePolicy.CASCADE) annotation should be enough to delete related Pages. How do you delete Issue? Can you provide a small project that reproduces the issue?

The problem is that if I don’t use CASCADE on JPA level the application does not save Page’s objects, because of:


org.springframework.dao.InvalidDataAccessApiUsageException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: net.gocoders.magazineappbackend.entity.Page-95e03344-1de8-5d78-8c3b-4f39ff19d3d5 [new].; nested exception is java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: net.gocoders.magazineappbackend.entity.Page-95e03344-1de8-5d78-8c3b-4f39ff19d3d5 [new].

Here is the Service Code:


import com.haulmont.cuba.core.Persistence;
import com.haulmont.cuba.core.Transaction;
import net.gocoders.magazineappbackend.entity.Issue;
import net.gocoders.magazineappbackend.entity.Page;
import org.springframework.stereotype.Service;

import javax.inject.Inject;
import java.util.List;

/**
 * @author arimolo
 */
@Service(IssueService.NAME)
public class IssueServiceBean implements IssueService {
    @Inject
    private Persistence persistence;

    @Override
    public void updatePages(Issue issue, List<Page> pages) {
        try (Transaction tx = persistence.createTransaction()) {
            // Search for existing customers with the given name
            issue = persistence.getEntityManager().find(Issue.class, issue.getId());
            issue.setPages(pages);
            tx.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

I also want to use orphanRemoval feature.
Do you see?

Thank you again! =)

So what is the problem now? Pages are not deleted when you delete Issue? But your Issue entity is SoftDelete, so it is not deleted from the JPA point of view, hence it does not propagate deletion to Pages.

No, the problem is that pages are not save when issue is saved with a new list of pages.
“new object was found through a relationship that was not marked cascade PERSIST”.

Earlier you said
The problem is that if I don’t use CASCADE on JPA level the application does not save Page’s objects, because of:
org.springframework.dao.InvalidDataAccessApiUsageException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: net.gocoders.magazineappbackend.entity.Page-95e03344-1de8-5d78-8c3b-4f39ff19d3d5 [new].; nested exception is java.lang.IllegalStateException: During synchronization a new object was found through a relationship that was not marked cascade PERSIST: net.gocoders.magazineappbackend.entity.Page-95e03344-1de8-5d78-8c3b-4f39ff19d3d5 [new].

So I thought that you have added CASCADE and it worked.

Yes, but you told me to do not use CASACADE on JPA level and I remove it.
First of all, do not use CASCADE on JPA level (in @OneToMany annotation) - it is not needed in CUBA and can cause issues.

I am trying to achieve all features combined:

  • Save the pages when saving the issue.
  • Delete the pages when delete the issue.
  • Delete the orphan pages when replace the list of pages on issue (orphanRemoval).

I am sorry, I have no intention in be a boring person here.

Please do not apologize, we are here to help you.

In fact, an idiomatic way of working with nested collections in CUBA is use of datasources. See the example here: https://doc.cuba-platform.com/manual-6.1/composition_recipe.html
The datasources send all modified instances to ORM separately and there is no need for CASCADE relationships on JPA level. And probably you will not need your service method too.

Alternatively, you can still try to use JPA CASCADE.

Anyway, would be great if you attach a small project with a part of your data model and a screen where we can reproduce your problem.

Thank you for the help! My intention is to help that amazing platform to grow too.

Here is my project code.
First you need to create an Issue, then use Upload PDF button to upload a PDF file that will be processed and the Pages objects should be created automatic by the service call.

MagazineAppBackend.zip (203.1K)

Hi Alexandre,

Thank you for the project. It helped me to understand your goal and suggest a solution.
What I have done:

  1. As you have made Issue hard-delete, I removed @OnDelete(DeletePolicy.CASCADE) as useless - it works only for soft-delete entities. So orphanRemoval = true is required for deleting Issues together with Pages.

  2. Created an Issue view that includes the collection of pages:

<view class="net.gocoders.magazineappbackend.entity.Issue"
      extends="_local"
      name="issue-view">
    <property name="pages"
              view="_local"/>
</view>
  1. In IssueBrowse controller, I reload the selected issue with the new view to load the list of pages and then pass it to issue-upload-pdf screen.
Issue issueWithPages = dataSupplier.reload(issuesTable.getSingleSelected(), "issue-view");
params.put("issue", issueWithPages);
openWindow("issue-upload-pdf", WindowManager.OpenType.DIALOG, params);
  1. In issue-upload-pdf screen, I created two datasources to hold the issue with pages:
<dsContext>
    <datasource id="issueDs"
                class="net.gocoders.magazineappbackend.entity.Issue"
                view="issue-view">
        <collectionDatasource id="pagesDs"
                              property="pages"/>
    </datasource>
</dsContext>
  1. In IssueUploadPdf controller:
  • set the received issue to the datasource:
_issue = (Issue)params.get("issue");
issueDs.setItem(_issue);
  • when creating a Page, set the reference to Issue:
Page pageObj = new Page();
pageObj.setIssue(_issue);
  • do not call service - it is no longer needed

  • in the background task done() method remove existing pages and add new ones to the datasource, then commit the whole DsContext (all datasources will be flushed in one transaction):

public void done(Void result) {
    for (Page page : pagesDs.getItems()) {
        pagesDs.removeItem(page);
    }
    for (Page page : pagesList) {
        pagesDs.addItem(page);
    }
    dsContext.commit();
  • you don’t need to use Vaadin’s safe methods for updating UI in done() and progress() methods - these are called in UI thread, see JavaDocs.

That’s all.
Actually I think you could do the same in the service if you set the reference to Issue in Pages when created them. But I like the solution with datasources more because it keeps all logic in one place. And additional benefit is that you can easily attach UI components to datasources and display results of your processing (Pages) right on this screen.

See the working project attached.

MagazineAppBackend.zip (149.1K)