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.

I'm running an online workshop on the 20-21 and 23-24 of November about High-Performance Java Persistence.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.