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:
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 thedetachedState
orhydratedState
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 11th of October about High-Performance SQL.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.
