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 type | Repeats this call on the children | What it does to the photos |
|---|---|---|
| CascadeType.PERSIST | em.persist() | Inserts new photos |
| CascadeType.MERGE | em.merge() | Copies changes and new photos from a detached album |
| CascadeType.REMOVE | em.remove() | Deletes the photos before the album |
| CascadeType.REFRESH | em.refresh() | Reloads the photos and drops unsaved changes |
| CascadeType.DETACH | em.detach() | Removes the photos from the persistence context |
| CascadeType.ALL | All five methods | All 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.

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 type | Status in Hibernate 7.4 |
|---|---|
| ALL, PERSIST, MERGE, REMOVE, REFRESH, DETACH | Same as the JPA types |
| LOCK | Exists; cascades lock() and has no JPA equivalent |
| REPLICATE | Deprecated, together with Session.replicate() |
| DELETE_ORPHAN | Internal; use orphanRemoval = true |
| SAVE_UPDATE | Removed in 7.0 with Session.saveOrUpdate() |
| DELETE | Removed 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.

For example, an album owns its photos, whereas the photos of a photographer stay in the archive when the photographer leaves the app.
| Association | Example | Cascade |
|---|---|---|
| @OneToMany, children owned by the parent | Album to photos | ALL with orphanRemoval = true |
| @OneToMany, children that outlive the parent | Photographer to photos | {PERSIST, MERGE} |
| @OneToOne, dependent detail | Photo to its camera settings | ALL with orphanRemoval = true |
| @ManyToOne | Photo to album | None |
| @ManyToMany | Photo to the people in it | None, 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.

// @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 runs | In Hibernate | In the database |
| SQL for an album with two photos | 1 select of the photos, 3 deletes | 1 delete |
| Photos loaded into memory | Yes | No |
| Needs the schema change | No | Yes, 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
- CascadeType JavaDoc (Jakarta Persistence 3.2)
- Jakarta Persistence 3.2 specification: Persisting an Entity Instance
- PessimisticLockScope JavaDoc (Jakarta Persistence 3.2)
- Hibernate ORM 7.4 User Guide: Cascading entity state transitions
- Hibernate ORM 7.4 JavaDoc: org.hibernate.annotations.CascadeType
- Hibernate ORM 7.0 Migration Guide: Session#save, update and saveOrUpdate
Happy Learning !!
“@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.”
Please refer this.
Excellent explanation of CascadeType.REMOVE vs Orphan Removal. Thank you
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’
What is a managed state?
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.
Hello. There is an error in first example of AccountEntity.java:
@OneToOne (mappedBy=”accounts”, fetch = FetchType.LAZY)
private EmployeeEntity employee;
it must be @ManyToOne
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?
Within same session, you should not save employee entity again. Refer: https://howtodoinjava.com/hibernate/hibernate-save-and-saveorupdate/