Follow @vlad_mihalcea Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

Introduction

As I previously explained, enterprise caching requires diligence. Because data is duplicated between the database (system of record) and the caching layer, we need to make sure the two separate data sources don’t drift apart.

If the cached data is immutable (neither the database nor the cache is able to modify it), we can safely cache it without worrying about any consistency issues. Read-only data is always a good candidate for application-level caching, improving read performance without having to relax consistency guarantees.

Read-only second-level caching

For testing the read-only second-level cache strategy, we going to use the following domain model:

The Repository is the root entity, being the parent of any Commit entity. Each Commit has a list of Change components (embeddable value types).

All entities are cached as read-only elements:

@org.hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_ONLY )

Persisting entities

In Hibernate 4, the read-only second-level cache uses a read-through caching strategy, entities being cached upon fetching.

doInTransaction(session -> { Repository repository = new Repository("Hibernate-Master-Class"); session.persist(repository); });

When an entity is persisted only the database contains a copy of this entity. The system of record is passed to the caching layer when the entity gets fetched for the first time.

@Test public void testRepositoryEntityLoad() { LOGGER.info("Read-only entities are read-through"); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); assertNotNull(repository); }); doInTransaction(session -> { LOGGER.info("Load Repository from cache"); session.get(Repository.class, 1L); }); }

This test generates the output:

--Read-only entities are read-through SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1 --JdbcTransaction - committed JDBC Connection --Load Repository from cache --JdbcTransaction - committed JDBC Connection

Once the entity is loaded into the second-level cache, any subsequent call will be served by the cache, therefore bypassing the database.

In Hibernate 5, READ_ONLY entities are write-through when using a SEQUENCE or a TABLE generator, while they are read-through for IDENTITY generator.

Updating entities

Read-only cache entries are not allowed to be updated. Any such attempt ends up with an exception being thrown:

@Test public void testReadOnlyEntityUpdate() { try { LOGGER.info("Read-only cache entries cannot be updated"); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); repository.setName( "High-Performance Hibernate" ); }); } catch (Exception e) { LOGGER.error("Expected", e); } }

Running this test generates the following output:

--Read-only cache entries cannot be updated SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1 UPDATE repository SET NAME = 'High-Performance Hibernate' WHERE id = 1 --JdbcTransaction - rolled JDBC Connection --ERROR Expected --java.lang.UnsupportedOperationException: Can't write to a read-only object

Because read-only cache entities are practically immutable it’s good practice to attribute them the Hibernate specific @Immutable annotation.

Deleting entities

Read-only cache entries are removed when the associated entity is deleted as well:

@Test public void testReadOnlyEntityDelete() { LOGGER.info("Read-only cache entries can be deleted"); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); assertNotNull(repository); session.delete(repository); }); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); assertNull(repository); }); }

Generating the following output:

--Read-only cache entries can be deleted SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; DELETE FROM repository WHERE id = 1 --JdbcTransaction - committed JDBC Connection SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; --JdbcTransaction - committed JDBC Connection

The remove entity state transition is enqueued by PersistenceContext, and at flush time, both the database and the second-level cache will delete the associated entity record.

Collection caching

The Commit entity has a collection of Change components.

@ElementCollection @CollectionTable( name="commit_change", joinColumns=@JoinColumn(name="commit_id") ) private List<Change> changes = new ArrayList<>();

Although the Commit entity is cached as a read-only element, the Change collection is ignored by the second-level cache.

@Test public void testCollectionCache() { LOGGER.info("Collections require separate caching"); doInTransaction(session -> { Repository repository = (Repository) session.get(Repository.class, 1L); Commit commit = new Commit(repository); commit.getChanges().add( new Change("README.txt", "0a1,5...") ); commit.getChanges().add( new Change("web.xml", "17c17...") ); session.persist(commit); }); doInTransaction(session -> { LOGGER.info("Load Commit from database"); Commit commit = (Commit) session.get(Commit.class, 1L); assertEquals(2, commit.getChanges().size()); }); doInTransaction(session -> { LOGGER.info("Load Commit from cache"); Commit commit = (Commit) session.get(Commit.class, 1L); assertEquals(2, commit.getChanges().size()); }); }

Running this test generates the following output:

--Collections require separate caching SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; INSERT INTO commit (id, repository_id) VALUES (DEFAULT, 1); INSERT INTO commit_change (commit_id, diff, path) VALUES (1, '0a1,5...', 'README.txt'); INSERT INTO commit_change (commit_id, diff, path) VALUES (1, '17c17...', 'web.xml'); --JdbcTransaction - committed JDBC Connection --Load Commit from database SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; SELECT changes0_.commit_id AS commit_i1_0_0_, changes0_.diff AS diff2_1_0_, changes0_.path AS path3_1_0_ FROM commit_change changes0_ WHERE changes0_.commit_id = 1 --JdbcTransaction - committed JDBC Connection --Load Commit from cache SELECT changes0_.commit_id AS commit_i1_0_0_, changes0_.diff AS diff2_1_0_, changes0_.path AS path3_1_0_ FROM commit_change changes0_ WHERE changes0_.commit_id = 1 --JdbcTransaction - committed JDBC Connection

Although the Commit entity is retrieved from the cache, the Change collection is always fetched from the database. Since the Changes are immutable too, we would like to cache them as well, to save unnecessary database round-trips.

Enabling Collection cache support

Collections are not cached by default, and to enable this behavior, we have to annotate them with the cache concurrency strategy:

@ElementCollection @CollectionTable( name="commit_change", joinColumns=@JoinColumn(name="commit_id") ) @org.hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_ONLY ) private List<Change> changes = new ArrayList<>();

Re-running the previous test generate the following output:

--Collections require separate caching SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; INSERT INTO commit (id, repository_id) VALUES (DEFAULT, 1); INSERT INTO commit_change (commit_id, diff, path) VALUES (1, '0a1,5...', 'README.txt'); INSERT INTO commit_change (commit_id, diff, path) VALUES (1, '17c17...', 'web.xml'); --JdbcTransaction - committed JDBC Connection --Load Commit from database SELECT readonlyca0_.id AS id1_2_0_, readonlyca0_.NAME AS name2_2_0_ FROM repository readonlyca0_ WHERE readonlyca0_.id = 1; SELECT changes0_.commit_id AS commit_i1_0_0_, changes0_.diff AS diff2_1_0_, changes0_.path AS path3_1_0_ FROM commit_change changes0_ WHERE changes0_.commit_id = 1 --JdbcTransaction - committed JDBC Connection --Load Commit from cache --JdbcTransaction - committed JDBC Connection

Once the collection is cached, we can fetch the Commit entity along with all its Changes without hitting the database.

Conclusion

Read-only entities are safe for caching and we can load an entire immutable entity graph using the second-level cache only. Because the cache is read-through, entities are cached upon being fetched from the database. The read-only cache is not write-through because persisting an entity only materializes into a new database row, without propagating to the cache as well.

Code available on GitHub.

Insert details about how the information is going to be processed DOWNLOAD NOW