Hibernate Tips: How to Prevent the Removal of a Parent Entity with Children
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
Hibernate Tips is a series of posts in which I describe a quick and easy solution for common Hibernate questions. If you have a question for a future Hibernate Tip, please post a comment below.
Question:
In one of the previous Hibernate Tips, I showed how to remove child entities automatically when you remove their parent. In the comments of that post, Jakob asked how to do the exact opposite. He wants to prevent the removal of entities that are referenced in an association:
“I am not allowed to delete a book that has a review. Is it possible to do that in Hibernate?”
Solution:
Yes, that’s possible. If you model the association on the Book entity, you can do that easily.
You can implement a lifecycle callback on the Book entity, that gets triggered before the entity gets removed. Within this method, you can access all attributes of the entity object. That enables you to check if the mapped association contains any elements. If it isn’t empty, you throw an exception to cancel the operation.
Another option would be to rely on a database constraint. You then don’t need to perform any validation in your Java application. This approach is efficient and easy to implement. But it also distributes the validation over multiple systems. That makes it harder to show a specific error message to the user.
Let’s assume that you want to perform all possible validations in your Java code. I nevertheless recommend adding the foreign key constraint on the database. You can then be absolutely sure that no referenced Book entity gets removed.
Mapping the Book entity
Here you can see a typical mapping of a Book entity.
@Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq") private Long id; @Version private int version; private String title; @OneToMany(mappedBy = "book") private List reviews = new ArrayList(); ... @PreRemove public void checkReviewAssociationBeforeRemoval() { if (!this.reviews.isEmpty()) { throw new RuntimeException("Can't remove a book that has reviews."); } } }
The id attribute maps the primary key. The @GeneratedValue annotation tells Hibernate to use the sequence book_seq to generate primary key values. Since Hibernate 5.3, you can do that without specifying the @SequenceGenerator. Hibernate then uses a database sequence that has the same name as your generator.
The version attribute is used by JPA’s optimistic locking mechanism to prevent concurrent updates. I explain it in great details in my Advanced Hibernate Online Training.
The title attribute maps a simple String to a database column.
The reviews attribute models the referencing side of a bidirectional many-to-one association. You need this attribute to implement the check in your lifecycle callback method.
The most interesting part of this entity is the checkReviewAssociationBeforeRemoval method. I annotated it with a @PreRemove annotation. This annotation tells your persistence provider to call this method before calling the remove method on the EntityManager.
Within this method, you can access all attributes of the entity object. You can use that to check if the reviews attribute contains any Review objects. If it does, you throw an Exception to cancel the remove operation. Please keep in mind that Hibernate might need to perform a database query to retrieve the associated Review entities if it did not already fetch them.
In this example, I throw a standard RuntimeException. But you could, of course, use one of your own business exceptions.
Testing the Book mapping
Let’s use the following test case to check that the lifecycle callback works as expected.
I first persist a new Book entity with a Review.
When I then try to remove that Book entity, Hibernate calls the checkReviewAssociationBeforeRemoval before it executes the remove method of the EntityManager. The Book entity references one Review. We, therefore, expect that the checkReviewAssociationBeforeRemoval method throws an exception. This will prevent Hibernate from removing the Book entity.
// Persist Book with 1 Review log.info("Persist Book with 1 Review"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); Book b = new Book(); b.setTitle("Hibernate Tips - More than 70 solutions to common Hibernate problems"); em.persist(b); Review r = new Review(); r.setComment("Amazing book!"); r.setBook(b); b.getReviews().add(r); em.persist(r); em.getTransaction().commit(); em.close(); // Try to remove Book log.info("Try to remove Book"); em = emf.createEntityManager(); em.getTransaction().begin(); b = em.find(Book.class, b.getId()); try { em.remove(b); Assert.fail("RuntimeException expected - Books with reviews can't be removed"); } catch (RuntimeException e) { log.info("Caught expected exception: "+e); }
When you activate the logging of SQL statements, you can see that the callback works as expected. The checkReviewAssociationBeforeRemoval method throws a RuntimeException, which prevents Hibernate from removing the Book entity.
07:41:26,982 INFO [org.thoughts.on.java.model.TestBidirectionalOneToMany] - Persist Book with 1 Review 07:41:27,274 DEBUG [org.hibernate.SQL] - select nextval ('book_seq') 07:41:27,283 DEBUG [org.hibernate.SQL] - select nextval ('book_seq') 07:41:27,342 DEBUG [org.hibernate.SQL] - select nextval ('review_seq') 07:41:27,349 DEBUG [org.hibernate.SQL] - select nextval ('review_seq') 07:41:27,374 DEBUG [org.hibernate.SQL] - insert into Book (title, version, id) values (?, ?, ?) 07:41:27,383 DEBUG [org.hibernate.SQL] - insert into Review (fk_book, comment, id) values (?, ?, ?) 07:41:27,395 INFO [org.thoughts.on.java.model.TestBidirectionalOneToMany] - Try to remove Book 07:42:49,786 DEBUG [org.hibernate.SQL] - select book0_.id as id1_0_0_, book0_.title as title2_0_0_, book0_.version as version3_0_0_ from Book book0_ where book0_.id=? 07:42:49,808 DEBUG [org.hibernate.SQL] - select reviews0_.fk_book as fk_book3_1_0_, reviews0_.id as id1_1_0_, reviews0_.id as id1_1_1_, reviews0_.fk_book as fk_book3_1_1_, reviews0_.comment as comment2_1_1_ from Review reviews0_ where reviews0_.fk_book=? 07:42:49,816 INFO [org.thoughts.on.java.model.TestBidirectionalOneToMany] - Caught expected exception: java.lang.RuntimeException: Can't remove a book that has reviews.
Learn more:
The following articles go into more details about JPA’s callback methods and other validation options:
- Hibernate Tips: How to automatically set an attribute before persisting it
- How to automatically validate entities with Hibernate Validator
- Hibernate Tips: Validate that only 1 of 2 associations is not null
- Hibernate Tips: How to perform different validations for persist and update
Hibernate Tips Book
Get more recipes like this one in my new book Hibernate Tips: More than 70 solutions to common Hibernate problems.
It gives you more than 70 ready-to-use recipes for topics like basic and advanced mappings, logging, Java 8 support, caching, and statically and dynamically defined queries.