How to map an immutable entity with JPA and Hibernate

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

In this article, you are going to find out how to map an immutable entity when using JPA and Hibernate.

If your Domain Model requires that a given entity should not be modified by the data access logic, Hibernate can enforce this requirement if the entity is marked with the @Immutable annotation.

Domain Model

Assuming you have the following Event entity:

ImmutableEvent entity

Because the Event entity is meant to be immutable, there is no setter method, and a single public constructor takes all the entity properties that need to be initialized:

@Entity(name = "Event")
@Immutable
public class Event {

    @Id
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_on")
    private Date createdOn = new Date();

    @Column(name = "event_key")
    private String eventKey;

    @Column(name = "event_value")
    private String eventValue;

    public Event(
            Long id, 
            String eventKey, 
            String eventValue) {
        this.id = id;
        this.eventKey = eventKey;
        this.eventValue = eventValue;
    }

    //Needed when instantiating the entity from a JDBC ResultSet
    private Event() {
    }

    //Getters omitted for brevity
}

Notice that the Event entity is marked with the @Immutable annotation since we don’t Hibernate to track Event entity modifications.

Preventing entity modifications

Even if the Event entity features no setter method, we can still change an Event entity instance via Java Reflection:

doInJPA(entityManager -> {
    Event event = entityManager.find(Event.class, 1L);

    assertEquals(
        "25", 
        event.getEventValue()
    );

    ReflectionUtils.setFieldValue(event, "eventValue", "10");
    
    assertEquals("10", event.getEventValue());
});

doInJPA(entityManager -> {
    Event event = entityManager.find(Event.class, 1L);

    assertEquals(
        "25", 
        event.getEventValue()
    );
});

However, when running the test case above, you will see that Hibernate issues no UPDATE statement since the Event entity is marked with the @Immutable annotation.

The reason why Hibernate does not track @Immutabale entity modifications is because the entity is loaded in read-only mode, hence the detachedState or hydratedState is never stored in the currently running Persistence Context.

JPQL update queries

Prior to Hibernate 5.2.17, JPQL queries were not taking into consideration the @Immutable status of a given entity.

In Hibernate 5.2.17, a WARNING message is logged when we try to modify the Event entity via a JPQL bulk update statement. Therefore, when running the following JPQL update query:

entityManager.createQuery(
    "update Event " +
    "set eventValue = :eventValue " +
    "where id = :id")
.setParameter("eventValue", "10")
.setParameter("id", 1L)
.executeUpdate();

Hibernate generates the following output:

WARN  HHH000487: The query: [update Event set eventValue = :eventValue where id = :id] attempts to update an immutable entity: [Event]

Query:["update Event set event_value=? where id=?"], Params:[(10, 1)]

Although the UPDATE statement is generated, there is now a WARNING message printed in the log.

If logging a WARN log entry is not sufficient for you and you want to prevent such modifications,
you can provide the following Hibernate configuration property:

<property
    name="hibernate.query.immutable_entity_update_query_handling_mode"
    value="exception"
/>

Now, when running the previous JPQL query, a `Hibernate exception is thrown:

try {
    doInJPA(entityManager -> {
        entityManager.createQuery(
            "update Event " +
            "set eventValue = :eventValue " +
            "where id = :id")
        .setParameter("eventValue", "10")
        .setParameter("id", 1L)
        .executeUpdate();
    });

    fail("Should have thrown exception");
} catch (Exception e) {
    HibernateException cause = (HibernateException) e.getCause();
	
    assertEquals(
        "The query: [update Event set eventValue = :eventValue where id = :id] " +
        "attempts to update an immutable entity: [Event]",
        cause.getMessage()
    );
}

Criteria API bulk update queries

The same goes for the Criteria API. By default, from Hibernate 5.2.17 onwards, a WARNING will be issued when executing the following Criteria API query:

CriteriaBuilder builder = entityManager
.getCriteriaBuilder();

CriteriaUpdate<Event> update = builder
.createCriteriaUpdate(Event.class);

Root<Event> root = update.from(Event.class);

update
.set(root.get("eventValue"), "100")
.where(
    builder.equal(root.get("id"), 1L)
);

entityManager
.createQuery(update)
.executeUpdate();

Writing JPA Criteria API queries is not very easy. The Codota IDE plugin can guide you on how to write such queries, therefore increasing your productivity.

For more details about how you can use Codota to speed up the process of writing Criteria API queries, check out this article.

Hibernate logs the following output:

-- HHH000487: The query: [update Event as generatedAlias0 set generatedAlias0.eventValue = :param0 where generatedAlias0.id=1L] attempts to update an immutable entity: [Event]

Query:["update Event set event_value=? where id=1"], Params:[(100)]

However, when switching to the exception mode:

<property
    name="hibernate.query.immutable_entity_update_query_handling_mode"
    value="exception"
/>

A HibernateException will be thrown to prevent the SQL UPDATE from being executed:

try {
    doInJPA(entityManager -> {
        CriteriaBuilder builder = entityManager
        .getCriteriaBuilder();
        
        CriteriaUpdate<Event> update = builder
        .createCriteriaUpdate(Event.class);

        Root<Event> root = update.from(Event.class);

        update
        .set(root.get("eventValue"), "100")
        .where(
            builder.equal(root.get("id"), 1L)
        );

        entityManager.createQuery(update).executeUpdate();
    });

    fail("Should have thrown exception");
} catch (Exception e) {
    HibernateException cause = (HibernateException) e.getCause();
    
    assertEquals(
        "The query: [" +
        "update Event as generatedAlias0 " +
        "set generatedAlias0.eventValue = :param0 " +
        "where generatedAlias0.id=1L" +
        "] attempts to update an immutable entity: [Event]",
        cause.getMessage()
    );
}

Cool, right?

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

Mapping an immutable entity with Hibernate is a trivial thing to do, and Hibernate 5.2.17 brings a new entity update query handling mode that prevents entity modifications via JPQL or Criteria API.

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.