A beginner’s guide to Hibernate flush operation order
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
As explained in this article, Hibernate shifts the developer mindset from SQL to entity state transitions. A JPA entity may be in one of the following states:
- New/Transient: the entity is not associated with a persistence context, be it a newly created object the database doesn’t know anything about.
- Persistent: the entity is associated with a persistence context (residing in the 1st Level Cache) and there is a database row representing this entity.
- Detached: the entity was previously associated with a persistence context, but the persistence context was closed, or the entity was manually evicted.
- Removed: the entity was marked as removed and the persistence context will remove it from the database at flush time.
Moving an object from one state to another is done by calling the EntityManager methods such as:
persist
merge
remove
Cascading allows propagating a given event from a parent to a child, also easing managing entities relationship management.
During the flush time, Hibernate will translate the changes recorded by the current Persistence Context into SQL queries.
Domain Model
Now, let’s consider we have the following entity:
@Entity(name = "Post") @Table( name = "post", uniqueConstraints = @UniqueConstraint( name = "slug_uq", columnNames = "slug" ) ) public class Post { @Id private Long id; private String title; @NaturalId private String slug; //Getters and setters omitted for brevity }
Notice the slug
property is marked with the @NaturalId
annotation since this represents a business key.
Now, let’s consider we have persisted the following Post
entity in our database:
Post post = new Post(); post.setId(1L); post.setTitle("High-Performance Java Persistence"); post.setSlug("high-performance-java-persistence"); entityManager.persist(post);
Testing time
Let’s assume you want to remove the existing Post
entity and persist a new instance with the same slug
attribute:
Post post = entityManager.find(Post.class, 1L); entityManager.remove(post); Post newPost = new Post(); newPost.setId(2L); newPost.setTitle("High-Performance Java Persistence Book"); newPost.setSlug("high-performance-java-persistence"); entityManager.persist(newPost);
If you try to do that, Hibernate will throw the following exception:
Query:["insert into post (slug, title, id) values (?, ?, ?)"], Params:[(high-performance-java-persistence, High-Performance Java Persistence Book, 2)] -- SQL Error: -104, SQLState: 23505 -- integrity constraint violation: unique constraint or index violation; SLUG_UQ table: POST
Hibernate did not execute the DELETE
first as we did in our test case. It executed the INSERT
statement first, and that’s why we get the ConstraintviolationException
.
You may wonder why this is happening since we are calling remove
prior to adding the second post
entity, and the answer is flush operations order.
Every entity state transition generates an action which is enqueued by the Persistence Context. You can see all the action queues in the ActionQueue
class which also provides the order of all the operations happening at flush time:
OrphanRemovalAction
AbstractEntityInsertAction
EntityUpdateAction
QueuedOperationCollectionAction
CollectionRemoveAction
CollectionUpdateAction
CollectionRecreateAction
EntityDeleteAction
So, the DELETE
statements are executed right at the end of the flush while the INSERT
statements are executed towards the beginning.
A hacky workaround
One way to work around this issue is to manual flush the Persistence Context after the remove
operation:
Post post = entityManager.find(Post.class, 1L); entityManager.remove(post); entityManager.flush(); Post newPost = new Post(); newPost.setId(2L); newPost.setTitle("High-Performance Java Persistence Book"); newPost.setSlug("high-performance-java-persistence"); entityManager.persist(newPost);
This will output the desired behavior:
Query:["delete from post where id=?"], Params:[(1)] Query:["insert into post (slug, title, id) values (?, ?, ?)"], Params:[(high-performance-java-persistence, High-Performance Java Persistence Book, 2)]
The proper fix
However, just because we can flush
the Persistence Context manually, it does not mean this is the right way to do it.
A manual flush
call is a code smell. In reality, you are better off updating the existing entity instead of removing and reinserting it back with the same business key:
Post post = entityManager.unwrap(Session.class) .bySimpleNaturalId(Post.class) .load("high-performance-java-persistence"); post.setTitle("High-Performance Java Persistence Book");
The UPDATE
will leave the index entries as is, while the remove-and-insert operation will incur additional work on the database side because both the table record and all index entries (Primary Key, the secondary index for slug
) must be removed only to be re-added back.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
Knowing the flush operation order is very important when using JPA and Hibernate. Because Hibernate executes the SQL statements in a strict order, JDBC batching can be applied automatically.
If you think you need to flush
the Persistence Context manually, think twice. You might have a remove-then-insert use case which is better handled by a single entity update instead.
