JPA Cascade Types: CascadeType.ALL, PERSIST, MERGE, REMOVE

A JPA cascade type repeats persist, merge, remove, refresh or detach from a parent entity to its children. See what each CascadeType does in Hibernate 7, the SQL it runs, what goes wrong without it, and which one to pick.

JPA Cascade Types

A JPA cascade type tells the EntityManager to repeat an operation on a parent entity for its associated entities, for example to save the photos of an album when we save the album. We set it with the cascade attribute of @OneToMany, @OneToOne, @ManyToOne or @ManyToMany. JPA defines six cascade types, namely PERSIST, MERGE, REMOVE, REFRESH, DETACH and ALL, and by default no operation cascades at all. CascadeType.ALL is the short form for the other five.

We use cascade types for children that belong to one parent, such as the photos of an album or the lines of an invoice, so one call on the parent saves or deletes the whole group.

The following example puts CascadeType.ALL on the photos of an album, so each EntityManager call on the album also reaches its photos.

@OneToMany(mappedBy = "album", cascade = CascadeType.ALL)
private List<Photo> photos = new ArrayList<>();
em.persist(album);    // insert into Album ..., then insert into Photo ... (once per photo)
em.merge(album);      // update Photo ..., update Album ...
em.remove(album);     // delete from Photo ... (once per photo), then delete from Album ...
em.refresh(album);    // reloads the album and its photos from the database
em.detach(album);     // the album and its photos are no longer managed

Notice that em.remove() deletes the photos before the album, so the foreign key from Photo to Album stays valid.

Next, we compare each cascade type with a mapping that has no cascade and pick the right type for each association. The FAQs cover the exceptions that a missing or wrong cascade causes.

1. What Are Cascade Types in JPA?

Each EntityManager method works on the one entity we pass to it. The call em.persist(album) saves the album row, and nothing else. A cascade type repeats that call to the entities the album references through an association, so one call handles the whole object graph.

The cascade attribute takes an array of CascadeType values. Each value matches one EntityManager method.

Cascade typeRepeats this call on the childrenWhat it does to the photos
CascadeType.PERSISTem.persist()Inserts new photos
CascadeType.MERGEem.merge()Copies changes and new photos from a detached album
CascadeType.REMOVEem.remove()Deletes the photos before the album
CascadeType.REFRESHem.refresh()Reloads the photos and drops unsaved changes
CascadeType.DETACHem.detach()Removes the photos from the persistence context
CascadeType.ALLAll five methodsAll five effects

The effect of each type shows best when we compare it with a mapping that has no cascade. Without a cascade, the operation stops at the album, and the photos either keep their old state or cause an error.

Grid with one row per cascade type. With the cascade, em.persist inserts the album and two photos, em.merge saves photo edits and a new photo, em.remove deletes the photos then the album, em.refresh reloads the photos, em.detach detaches them. Without a cascade, persist saves only the album, merge loses photo edits, remove fails with a foreign key violation, refresh keeps photo edits and flushes them, and detach leaves the photos managed
Without a cascade, only REMOVE fails with an error. The other four go wrong without an error, so photos or edits are lost, or edits we wanted to drop are saved.

Two rules apply to every cascade type.

  • No operation cascades unless we list it. The default value of cascade is an empty array.
  • A cascade goes in one direction, from the entity that declares it to the entities it references. We put it on the parent side, here Album.photos, even though Photo holds the foreign key.

To cascade more than one operation, we list the types in braces.

@OneToMany(mappedBy = "album", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Photo> photos = new ArrayList<>();

2. Cascade Types Example

The following example is a photo app that runs on Hibernate 7.4, Java 25 and an in-memory H2 database. The project on GitHub has one package per cascade type, each with the same Album and Photo entities, so each case runs against a mapping that has only that cascade type.

2.1. Album and Photo Entities

An album has many photos, and each photo belongs to one album. The association is a bidirectional @OneToMany, where the photo table holds the album_id foreign key and Album declares mappedBy to say that Photo.album owns the link.

@Id
@GeneratedValue
private Long id;

private String title;

@OneToMany(mappedBy = "album", cascade = CascadeType.PERSIST)   // the type changes per package
private List<Photo> photos = new ArrayList<>();

public void addPhoto(Photo photo) {
  photos.add(photo);
  photo.setAlbum(this);
}
@Id
@GeneratedValue
private Long id;

private String title;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id")
private Album album;

The addPhoto() helper sets both sides of the association. Hibernate writes album_id from Photo.album, so a photo added only to the list would be saved without its album.

2.2. CascadeType.PERSIST

The type PERSIST saves new photos together with a new album, so we call em.persist() only once, on the album.

Album goa = new Album("Goa Trip");
goa.addPhoto(new Photo("Beach"));
goa.addPhoto(new Photo("Sunset"));
em.persist(goa);    // insert into Album (title,id) values (?,?)
                    // insert into Photo (album_id,title,id) values (?,?,?)
                    // insert into Photo (album_id,title,id) values (?,?,?)

The cascade also runs at flush time. A new photo added to a managed album is inserted at commit without its own persist() call.

Album goa = em.find(Album.class, albumId);
goa.addPhoto(new Photo("Dinner"));    // insert into Photo (album_id,title,id) values (?,?,?) at commit

Without PERSIST, Hibernate inserts only the album and ignores the photos, with no error and no warning. The transaction commits, and the photo table stays empty.

2.3. CascadeType.MERGE

MERGE matters when the album is a detached entity, for example an object loaded in one transaction, changed in a web form and saved in another. The method em.merge() copies the album’s state onto a managed copy, and MERGE does the same for each photo.

// goa was loaded with its photos in an earlier transaction
goa.setTitle("Goa 2026");
goa.findPhoto("Beach").setTitle("Beach Day");
goa.addPhoto(new Photo("Boat"));

em.merge(goa);    // insert into Photo (album_id,title,id) values (?,?,?)
                  // update Photo set album_id=?,title=? where id=?
                  // update Album set title=? where id=?

Without MERGE, only update Album runs. The photo keeps the title “Beach”, and the “Boat” photo is never inserted.

2.4. CascadeType.REMOVE

REMOVE deletes the photos when we delete the album with em.remove(). Hibernate loads the photos first, and deletes the children before the parent, so the foreign key is never broken.

em.remove(em.find(Album.class, albumId));
// select ... from Photo p1_0 where p1_0.album_id=?
// delete from Photo where id=?
// delete from Photo where id=?
// delete from Album where id=?

Without REMOVE, Hibernate deletes only the album row. The photos still point to it, so the database rejects the statement and the transaction rolls back.

jakarta.persistence.RollbackException: Error while committing the transaction [could not execute statement [Referential integrity constraint violation: "FKF3NAQFW4X3LG9C5CG1P9I2JGQ: PUBLIC.PHOTO FOREIGN KEY(ALBUM_ID) REFERENCES PUBLIC.ALBUM(ID) (CAST(1 AS BIGINT))"; SQL statement:
delete from Album where id=? [23503-252]] [delete from Album where id=?]]

REMOVE reacts only to em.remove() on the album. Removing a photo from the photos list is a different action, which section 2.7 explains.

2.5. CascadeType.REFRESH

REFRESH reloads the photos when we refresh the album. The method em.refresh() reads the rows again and overwrites the unsaved changes in memory.

Album goa = em.find(Album.class, albumId);
Photo beach = goa.findPhoto("Beach");
goa.setTitle("Goa 2026");
beach.setTitle("Beach Day");

em.refresh(goa);                        // select a1_0.id,a1_0.title,p1_0.album_id,p1_0.id,p1_0.title
                                        //   from Album a1_0 left join Photo p1_0 on a1_0.id=p1_0.album_id where a1_0.id=?
String albumTitle = goa.getTitle();     // "Goa Trip"
String photoTitle = beach.getTitle();   // "Beach"

Without REFRESH, Hibernate reloads only the album. The call beach.getTitle() still returns “Beach Day”, and at commit Hibernate writes it to the database with update Photo … where id=?. A refresh without the cascade can save the very changes we wanted to throw away.

2.6. CascadeType.DETACH

DETACH removes the photos from the persistence context together with the album. Hibernate stops tracking them, so later changes are not saved.

Album goa = em.find(Album.class, albumId);
Photo beach = goa.findPhoto("Beach");
em.detach(goa);

boolean albumManaged = em.contains(goa);     // false
boolean photoManaged = em.contains(beach);   // false
beach.setTitle("Beach Day");                 // no SQL at commit

Without DETACH, em.contains(beach) returns true. The photo is still a managed entity, and its new title is saved at commit.

2.7. CascadeType.ALL

CascadeType.ALL is a short form for {PERSIST, MERGE, REMOVE, REFRESH, DETACH}. We use it when the children never exist without the parent, as with the photos of an album.

@OneToMany(mappedBy = "album", cascade = CascadeType.ALL)
private List<Photo> photos = new ArrayList<>();

Even ALL does not delete a photo that we only remove from the list. Hibernate sets the photo’s foreign key to NULL, and the row stays.

goa.removePhoto(goa.findPhoto("Sunset"));   // update Photo set album_id=?,title=? where id=?
                                            // the Sunset row stays, with album_id = NULL

To delete the orphan row, we add orphanRemoval = true next to the cascade, which differs from CascadeType.REMOVE because it also reacts to removing a photo from the list.

2.8. Hibernate’s Own Cascade Types With @Cascade

Hibernate has a second enum, org.hibernate.annotations.CascadeType, used with the @Cascade annotation. It repeats the JPA types and adds a few types for operations that only the Hibernate Session has. Hibernate 7 removed or deprecated most of those extra types.

Hibernate cascade typeStatus in Hibernate 7.4
ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACHSame as the JPA types
LOCKExists; cascades lock() and has no JPA equivalent
REPLICATEDeprecated, together with Session.replicate()
DELETE_ORPHANInternal; use orphanRemoval = true
SAVE_UPDATERemoved in 7.0 with Session.saveOrUpdate()
DELETERemoved in 7.0 with Session.delete(); use REMOVE

The @Cascade annotation itself is deprecated for removal since Hibernate 7, and Hibernate recommends the JPA cascade attribute instead. The type LOCK is the only reason left to write @Cascade.

@OneToMany(mappedBy = "album", cascade = CascadeType.PERSIST)
@Cascade(org.hibernate.annotations.CascadeType.LOCK)
private List<Photo> photos = new ArrayList<>();

The name suggests that locking the album also locks the photos, but it does not. A pessimistic lock locks only the album row, even with the photos loaded. To lock the photo rows too, we pass PessimisticLockScope.EXTENDED, which works with or without the cascade.

em.lock(goa, LockModeType.PESSIMISTIC_WRITE);
// select a1_0.id from Album a1_0 where a1_0.id=? for update

em.lock(goa, LockModeType.PESSIMISTIC_WRITE, PessimisticLockScope.EXTENDED);
// select a1_0.id from Album a1_0 where a1_0.id=? for update
// select tbl.album_id from Photo tbl where tbl.album_id=? for update

In Hibernate 5, session.lock(entity, LockMode.NONE) was a common way to reattach a detached entity and its children. Hibernate 7.4 rejects a detached entity with IllegalArgumentException: org.hibernate.DetachedObjectException: Given entity is not associated with the persistence context. Use em.merge() instead.

3. Which Cascade Type Do I Need?

The choice depends on two questions. The first is whether the cascade reaches an entity that other entities share, and the second is whether the child can exist without its parent.

Decision tree: if the association is ManyToOne or ManyToMany, never use REMOVE or ALL, PERSIST and MERGE at most; otherwise, if the child can exist without the parent, as a photo outlives its photographer, use PERSIST and MERGE and delete or move the children yourself; if it cannot, use CascadeType.ALL plus orphanRemoval = true, and for many children per parent consider @OnDelete(CASCADE) instead of REMOVE
Start from the association type. REMOVE belongs only on a parent that owns its children.

For example, an album owns its photos, whereas the photos of a photographer stay in the archive when the photographer leaves the app.

AssociationExampleCascade
@OneToMany, children owned by the parentAlbum to photosALL with orphanRemoval = true
@OneToMany, children that outlive the parentPhotographer to photos{PERSIST, MERGE}
@OneToOne, dependent detailPhoto to its camera settingsALL with orphanRemoval = true
@ManyToOnePhoto to albumNone
@ManyToManyPhoto to the people in itNone, or {PERSIST, MERGE}

3.1. Why Not Cascade REMOVE on @ManyToOne or @ManyToMany?

On these associations, the cascade goes from the child to an entity that other children also use. Let us put REMOVE on the child side of both associations to see what goes wrong.

@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
@JoinColumn(name = "album_id")
private Album album;

@ManyToMany(cascade = CascadeType.REMOVE)
@JoinTable(name = "photo_person", ...)
private Set<Person> people = new HashSet<>();

With these mappings, deleting one photo also tries to delete its album and every person in the photo.

Two panels. Left: with REMOVE on ManyToOne, em.remove(beach) deletes the photo and then tries to delete album Goa Trip, which fails with a foreign key violation because Sunset still points to it; em.remove(cake) deletes the Party album with its last photo. Right: with REMOVE on ManyToMany, em.remove(beach) deletes the link rows and tries to delete Lokesh, which fails because Sunset still links to Lokesh; em.remove(cake) deletes Alex together with the photo
When the album or person is still used, the delete fails. When it is not, Hibernate deletes it without any error, which is worse.
// @ManyToOne(cascade = REMOVE)
em.remove(beach);   // delete from Photo where id=?
                    // delete from Album where id=?   -> FK violation, Sunset still uses the album
em.remove(cake);    // delete from Photo where id=?
                    // delete from Album where id=?   -> the Party album is gone

// @ManyToMany(cascade = REMOVE)
em.remove(beach);   // delete from photo_person where photo_id=?
                    // delete from Person where id=?  -> FK violation, Sunset still shows Lokesh
em.remove(cake);    // delete from photo_person where photo_id=?
                    // delete from Person where id=?
                    // delete from Photo where id=?   -> Alex is gone

The failure is the lucky case. When nothing else references the album or the person, the data is deleted, and nobody notices until a user asks where the “Party” album or the person “Alex” went.

4. Cascade Type FAQs

Most cascade questions start from an exception at commit, or from a framework method that calls persist() and merge() for us.

4.1. Why Does persist() Fail for a Photo That Already Exists?

PERSIST cascades only to new entities. When we add a photo loaded in an earlier transaction to a new album, the cascade calls persist() on that detached photo.

Album best = new Album("Best Of");
best.addPhoto(beach);          // beach was loaded in another transaction
em.persist(best);
jakarta.persistence.EntityExistsException: Detached entity passed to persist: com.howtodoinjava.hibernate.cascade.persist.Photo

There are two fixes, and both move the photo to the new album.

  • Load the photo in the same transaction with em.find() or a query, so it is managed and persist() skips it.
  • Call em.merge(best) instead of persist(), with MERGE in the cascade list.

4.2. What Causes TransientPropertyValueException?

Hibernate throws TransientPropertyValueException at flush when a managed entity references a new entity that nobody persisted. For example, we persist a photo whose album is new, and Photo.album has no cascade.

Album goa = new Album("Goa Trip");
Photo beach = new Photo("Beach");
goa.addPhoto(beach);
em.persist(beach);
org.hibernate.TransientPropertyValueException: Persistent instance of 'com.howtodoinjava.hibernate.cascade.none.Photo' references an unsaved transient instance of 'com.howtodoinjava.hibernate.cascade.none.Album' (persist the transient instance before flushing)

The fix is to persist the parent, either with em.persist(goa) and PERSIST on Album.photos, or by persisting the album before the photo.

4.3. What Is the Difference Between CascadeType.REMOVE and @OnDelete?

CascadeType.REMOVE is a JPA feature. Hibernate loads every photo and sends one DELETE per row. Hibernate’s @OnDelete annotation adds on delete cascade to the foreign key, and the database deletes the photos itself.

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "album_id")
@OnDelete(action = OnDeleteAction.CASCADE)
private Album album;
alter table if exists Photo add constraint FKf3naqfw4x3lg9c5cg1p9i2jgq foreign key (album_id) references Album on delete cascade

delete from Album where id=?
CascadeType.REMOVE@OnDelete(action = CASCADE)
Where it runsIn HibernateIn the database
SQL for an album with two photos1 select of the photos, 3 deletes1 delete
Photos loaded into memoryYesNo
Needs the schema changeNoYes, on the foreign key

4.4. What Replaced CascadeType.SAVE_UPDATE in Hibernate 7?

SAVE_UPDATE cascaded the old Session.save(), update() and saveOrUpdate() methods. Hibernate 7.0 removed those methods and the cascade type. We use em.persist() for new entities, em.merge() for detached ones, and the matching JPA cascade types.

// Hibernate 5/6
@Cascade(org.hibernate.annotations.CascadeType.SAVE_UPDATE)

// Hibernate 7
@OneToMany(mappedBy = "album", cascade = {CascadeType.PERSIST, CascadeType.MERGE})

4.5. Which Cascade Types Does Spring Data JPA save() Need?

The method repository.save() calls em.persist() for a new entity and em.merge() for an existing one. So the parent needs PERSIST to insert new children with a new parent, and MERGE to save child changes when the parent already has an id. Either {PERSIST, MERGE} or ALL covers both.

5. Conclusion

A cascade type repeats persist(), merge(), remove(), refresh() or detach() from a parent to its children, and nothing cascades unless we list it. Without the right cascade, Hibernate often fails without an error, so new photos or merged changes are lost, or a refresh saves edits we meant to discard. We put CascadeType.ALL with orphanRemoval on parents that own their children, {PERSIST, MERGE} on parents whose children live on, and never REMOVE on @ManyToOne or @ManyToMany. Hibernate’s own @Cascade annotation is deprecated for removal. Only LOCK still needs it, and LOCK does not lock the children’s rows.

6. References

Happy Learning !!

Source Code on Github

Leave a Comment

  1. “@ManyToOne (mappedBy=”accounts” ”

    Is this OK? Cause jakarta-persistence-api-3.1.0 OneToMany @interface doesn’t support mappedBy !

    To send this, on this site there are a couple of rules contradicting this solution:
    https://medium.com/@rajibrath20/the-best-way-to-map-a-onetomany-relationship-with-jpa-and-hibernate-dbbf6dba00d3

    “Bidirectional relationships must follow these rules:

    The many side of @ManyToOne bidirectional relationships must not define the mappedBy element. The many side is always the owning side of the relationship.”

  2. I have some questions :

    1) the relationship between Account and Employee is ‘1 to N’ , ans as per the rule Many side has to be the owner. which means 1 side has to have @mappedBy annotation. So EmployeeEntity.java should have @mappedBy annotation. in your first example, you have used @mappedBy in AccountEntity.java class

    2) why you have used @OneToOne in AccountEntity .java in your first example , the relationship is ‘1to N’

  3. I feel AccountDetail is owner of relationship hence you should mention @JoinColumn(name=”employeeId”) etc. instead of EmployeeEntity which should have “mappedBy=accoutId” , @OneToMany(orphanRemoval = true, mappedBy = “employee”)
    private Set accounts; seems to be wrong, just check you set “employee” over Set of accounts.

  4. Hello. There is an error in first example of AccountEntity.java:
    @OneToOne (mappedBy=”accounts”, fetch = FetchType.LAZY)
    private EmployeeEntity employee;

    it must be @ManyToOne

  5. Hey I am facing one issue of datached entity. Scenario in addition to you example Account has OneToMany relationship with some other entity like addresses. I am fetching Employee entity and from its account list I want to delete specific account. I remove that account from list and save Employee. But while saving Employee I am getting detached entity error for addresses of deleted account.
    Do you know how to resolve this issue?

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.