How to map an immutable entity with JPA and Hibernate

(Last Updated On: April 4, 2018)

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:

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 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();

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?

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.

Subscribe to our Newsletter

* indicates required
10 000 readers have found this blog worth following!

If you subscribe to my newsletter, you'll get:
  • A free sample of my Video Course about running Integration tests at warp-speed using Docker and tmpfs
  • 3 chapters from my book, High-Performance Java Persistence, 
  • a 10% discount coupon for my book. 
Get the most out of your persistence layer!

Advertisements

6 thoughts on “How to map an immutable entity with JPA and Hibernate

  1. your class is not immutable since you are likely exposing the createdOn field which is a java.util.Date. Since the Date class is mutable, you could still modify your Event object, unless the getter takes care to return a copy of the Date object.

    1. You assumed that the @Immutable annotation on a JPA entity is the same with an immutable Java object, which is not true.

      If you read the article carefully, you’ll see the following comment:

      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.

  2. Is there a performance benefit of immutable entities? I guess that these entites do not need to ber dirty checked, so hibernate does not need to keep copies of there retrieved object from the DB, so does that mean that by using @Immutable the memory footprint is reduced by 50%?

      1. Any idea how i could showcase this to my colleagues? I would insert 1000 lets say Test entities in a database and then select them. Then i would open up jvisualvm and show them they consume x bytes of memory. But where do i find the ones that are used by hibernate for dirty checking?

      2. There’s a detachedState array that goes into the EntityEntry in the PersistenceContext class. Best to how it with 100k entries and compare the heap memory consumption.

Leave a Reply

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