The labs on this site need JavaScript to run. The same content is written out below.
Learn › JPA › Relationships
Cascade and orphan removal
What happens to the children when you save or delete only the parent?
A post (the parent) has two comments (the children). Whether the comments are saved and deleted together when you persist or remove only the post is decided by cascade and orphanRemoval on @OneToMany.
Checked against real execution · 24 combinations were run on Hibernate 7.4.5 and H2, and the number and order of the SQL statements, the exceptions, and the rows left in the DB were checked
Concepts in this lab
- Cascade
- A setting that passes an operation performed on a parent entity on to its associated child entities in the same way. You write it in the cascade attribute of an association annotation such as @OneToMany or @OneToOne. CascadeType.PERSIST passes on saving, REMOVE passes on deletion, and ALL passes on every operation. Without it, persisting only the parent does not save the children, and removing a parent that still has children fails because of the foreign key constraint. It is a convenience that passes operations on, not a feature that sets up the relationship, and you use it when only that one parent owns the children.
- Orphan removal (orphanRemoval)
- A setting that also deletes from the DB a child entity that has been disconnected from its parent entity. You write it as orphanRemoval = true on @OneToMany or @OneToOne. If you take a child out of the parent's collection, a DELETE for that child is sent when the transaction commits. Without this setting, the row in the DB stays even if you take the child out of the collection. Children are also deleted when the parent is deleted, so you can think of it as including the behavior of CascadeType.REMOVE. Do not use it for a child that other entities also refer to.
- Persistence context
- The logical space in JPA that holds and manages entities. Entities that you load or save are kept here, and JPA tracks the changes made to them and applies those changes to the database when the transaction ends. It usually shares its lifecycle with one transaction (in a web request with Open Session in View (OSIV) turned on, it lasts until the request ends). Features such as the first-level cache, dirty checking, and transactional write-behind work on top of it.
- Transactional write-behind and flush
- Transactional write-behind means that INSERT, UPDATE, and DELETE statements are not sent to the database right away. They are collected and sent together. However, for an entity whose primary key is generated with the IDENTITY strategy (such as AUTO_INCREMENT in MySQL), the INSERT runs immediately when you request the save, because the identifier must be obtained. A flush sends the collected changes to the database. It happens automatically just before the transaction commits and just before a JPQL query runs. A flush only sends SQL to the database. It does not commit the transaction.
- Entity
- A Java object that corresponds to a row of a database table. It is defined as a class annotated with @Entity and has an identifier (@Id). JPA tracks the state changes of these objects and runs the matching SQL.
Try these
- Default settings — Persist only the post, with no cascade. There is no exception, but only the comments are left out of the save.
- CascadeType.PERSIST — The persist is passed on to the comments, so 3 INSERTs are sent.
- Remove the post without cascade — The comments point to the post, so the DB refuses the deletion.
- Remove with CascadeType.REMOVE — It deletes the comments first and then the post.
- ALL, but you only take one out of the list — Even with CascadeType.ALL, no DELETE is sent.
- orphanRemoval = true — A DELETE is sent for the comment you took out of the list.
- Only orphanRemoval on, and remove the post — The comments are deleted together even though you did not write cascade.
Running with the default settings
- new Post, addComment twice: Created one post object and two comment objects and connected them to each other. They are still ordinary Java objects that JPA does not know about (New). The DB has nothing.
- em.persist(post): The post comes under the management of the persistence context. The primary key uses the IDENTITY strategy, so the INSERT is sent right away to get the id.
- Because cascade does not have PERSIST, the persist stops at the post. The two comments in the list are still New objects that JPA does not know about.
- Method ends, commit: It commits without an exception. comments is the side with mappedBy, so it does not manage the foreign key, and Hibernate does not complain even if this list holds objects that have not been saved. The comment INSERTs were never sent.
Result of this run
- Without PERSIST (or ALL) in cascade, the persist you call on the parent is not passed on to the children. As in this example, with a list that has mappedBy, no exception is thrown either, so it is easy to miss that the comments were not saved.
Code
@Entity
class Post {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String title;
@OneToMany(mappedBy = "post")
List<Comment> comments = new ArrayList<>();
void addComment(String text) { // sets both sides together
Comment c = new Comment(text);
c.post = this;
comments.add(c);
}
}
@Entity
class Comment {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
String text;
@ManyToOne(fetch = FetchType.LAZY)
Post post; // foreign key post_id
}
// Runs inside a transaction
Post post = new Post("Notice");
post.addComment("First comment");
post.addComment("Second comment");
em.persist(post); // persists only the post
// The commit happens when the method ends
Assumptions of this model
This lab assumes a bidirectional relationship (@OneToMany(mappedBy) and @ManyToOne), a primary key with the IDENTITY strategy, and execution inside a single transaction. All 24 combinations (4 cascade settings, 2 orphanRemoval settings, and 3 actions) were run on real Hibernate 7.4.5 and an H2 database, and the number and order of the SQL statements sent, the exception, and the number of rows left in the DB after it ended were confirmed to match the screen. The SQL statements on the screen are shortened to be easy to read and are not letter-for-letter the same as the actual statements. The result that "if you persist only the parent without cascade, the children are left out without an exception" holds for this mapping, where the comment list has mappedBy. In the same environment, it was confirmed that if you change it to a unidirectional @OneToMany(@JoinColumn), where the list side manages the foreign key directly, and run the same code, a TransientPropertyValueException is thrown at commit and the post is not saved either. The opposite case, persisting only a comment that points to a post that has not been saved, is not covered in this lab. If the primary key uses the SEQUENCE strategy, the INSERTs are sent at commit, not when you call persist (this is covered in the "Persistence context" lab). Setting ON DELETE CASCADE on the foreign key in the DB is a different feature from cascade in JPA and is not covered here.
Previous lab: Inheritance mapping · Next lab: Proxies and lazy fetching