Using the Java Persistence API and a decent persistence provider allows us to configure and fine-tune when and how the second level cache is used in our application.
In the following short examples, we’re going to demonstrate those features written as JUnit test cases and running on a H2 in-memory database.
Setup
First of all we need some basic setup to run the following examples .. we need to select a JPA persistence provider and database, create a persistence-unit configuration and an environment to run tests on an in-memory database.
For all readers who have done this a bazillion times before – and I guess they have – please feel free to skip directly to the section Second Level Cache Modes.
Maven Dependencies
I’m using EclipseLink as the persistence provider and a H2 in-memory database for the following examples, so these dependencies are part of my pom.xml.
In addition I’m using JUnit (junit:junit:4.11) and Hamcrest (org.hamcrest:hamcrest-all:1.3) to write the test cases.
<dependency>
<groupId>org.eclipse.persistence</groupId>
<artifactId>eclipselink</artifactId>
<version>2.5.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.177</version>
</dependency>
Persistence Configuration
The persistence provider is configured in the following persistence.xml put in src/main/resources/META-INF.
Nothing special here excepting the shared-cache-element explained below .. the other configuration is there to specify EclipseLink as designated persistence provider, to use an in-memory H2 database, to create the tables automatically derived from our specified entities and to scan for entities in the current classpath so that we don’t have to write a bunch of class-elements into the persistence descriptor.
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence persistence_2_1.xsd" version="2.1">
<persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
<provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
<shared-cache-element>ENABLE_SELECTIVE</shared-cache-element>
<properties>
<property name="javax.persistence.target-database" value="h2" />
<property name="javax.persistence.jdbc.driver" value="org.h2.Driver" />
<property name="javax.persistence.jdbc.url" value="jdbc:h2:mem:testdb;create=true" />
<property name="javax.persistence.jdbc.user" value="" />
<property name="javax.persistence.jdbc.password" value="" />
<property name="javax.persistence.sharedCache.mode" value="ALL"/>
<property name="eclipselink.ddl-generation" value="create-tables" />
<property name="eclipselink.logging.level" value="FINE" />
</properties>
<exclude-unlisted-classes>false</exclude-unlisted-classes>
</persistence-unit>
</persistence>
Test Bootstrap
Each test is set up using the following bootstrap code. I’ve omitted it in the examples below to avoid repetition:
EntityManagerFactory emf;
EntityManager em;
EntityTransaction tx;
@Before
public void setup() {
emf = Persistence.createEntityManagerFactory("default");
em = emf.createEntityManager();
tx = em.getTransaction();
}
@After
public void teardown() {
em.close();
emf.close();
}
Second Level Cache Modes
You may control the general cache mode for a persistence unit by adding declaring “shared-cache-element” with one of the following values to your persistence.xml file.
Alternatively you may specify the mode when creating the EntityManagerFactory e.g.:
EntityManagerFactor emf = Persistence.createEntityManagerFactory(
"default",
new Properties().add(
"javax.persistence.sharedCache.mode", "ENABLE_SELECTIVE")
);
-
ALL: For this persistence unit, all entity data is stored in the 2nd level cache
-
NONE: No data is cached
-
ENABLE_SELECTIVE: Entities marked with @Cachable are cached (see examples below)
-
DISABLE_SELECTIVE: All entities are cached, excepting entities marked with @Cachable(false) (see examples below)
-
UNSPECIFIED: The caching behaviour is not specified, the persistence provider’s default caching behaviour is used
As I’m using selective caching for the following examples, I’m adding the following declaration to my persistence.xml:
<shared-cache-element>ENABLE_SELECTIVE</shared-cache-element>
Specify Cache Retrieval and Store Modes
When the second level cache is enabled for a persistence unit, we’re now able fine-tune the caching behaviour by setting retrieval- and store-mode-properties at persistence context level or for each entity-manager operation or query level.
Retrieval Mode
This mode defines how data is read from the cache when using queries or calling the entitymanager’s find method.
We may modify the mode by setting the property javax.persistence.retrieveMode to a value of the enum javax.persistence.CacheRetrieveMode:
-
BYPASS: The cache is bypassed and a call to the database is used to retrieve the data.
-
USE: If the data is available in the cache, it is read from this location, else it is fetched from the database
Example bypassing the cache for a lookup:
EntityManager em=...
Map<String, Object> properties = new HashMap<String, Object>();
properties.put("javax.persistence.cache.retrieveMode", "BYPASS");
Long bookId = 1L;
Book book = em.find(Book.class, bookId, properties);
Store Mode
This mode defines how data is stored in the cache.
We may modify the mode by setting the property javax.persistence.storeMode to a value of the enum javax.persistence.CacheStoreMode:
-
BYPASS: Don’t put anything into the cache
-
REFRESH: Data is put/updated in the cache when read and committed into the database a refresh enforced
-
USE: Data is put/updated in the cache when read and committed into the database
Example: Selective Caching
In this example, we’re going to explore how to achieve selective caching for specific entities using a combination of shared-cache-element = ENABLE_SELECTIVE and the class level annotation @Cachable.
Book Entity
This is our book entity .. nothing special here excepting the enforced caching using java.persistence.Cachable set to true at class level.
package com.hascode.tutorial.jpa_caching.entity;
import java.io.Serializable;
import javax.persistence.Cacheable;
import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
@Cacheable(true)
public class Book implements Serializable {
@Id
private Long id;
private String title;
// constructor, getter, setter ..
}
Test Case 1: Enforced Caching in Action
We’re persisting two book entities here, force the commit on the entity transaction and get a handle to the cache from the entity-manager-factory afterwards.
As we can see, both entities are present in the cache. Now we’re removing one entity by another from the cache and verify for each that they are not present anymore on the cache.
@Test
public void shouldCacheACachableEntity() throws Exception {
tx.begin();
Book book1 = new Book(1L, "Some book");
Book book2 = new Book(2L, "Another book");
em.persist(book1);
em.persist(book2);
tx.commit();
Cache cache = emf.getCache();
assertThat(cache.contains(Book.class, 1L), is(true));
assertThat(cache.contains(Book.class, 2L), is(true));
Book cachedBook = em.find(Book.class, 1L);
assertThat(cachedBook, notNullValue());
cache.evict(Book.class, 1L); // clear one designated book from cache
assertThat(cache.contains(Book.class, 1L), is(false));
cache.evict(Book.class); // clear all books from cache
assertThat(cache.contains(Book.class, 2L), is(false));
}
Person Entity
Again nothing special here excepting that we’ve set @Cachable to false.
package com.hascode.tutorial.jpa_caching.entity;
[..]
@Entity
@Cacheable(false)
public class Person implements Serializable {
@Id
private Long id;
private String name;
// constructor, getter, setter ommitted ..
}
Test Case 2: Enforced Cache Bypass
A similar scenario as above but this time, the entities are not cached neither after being persisted nor after a search for them.
@Test
public void shouldNotCacheAnUncachableEntity() throws Exception {
tx.begin();
Person person1 = new Person(1L, "Lisa");
Person person2 = new Person(2L, "Tim");
em.persist(person1);
em.persist(person2);
tx.commit();
Cache cache = emf.getCache();
assertThat(cache.contains(Person.class, 1L), is(false));
assertThat(cache.contains(Person.class, 2L), is(false));
Person personFound = em.find(Person.class, 1L);
assertThat(personFound, notNullValue());
}
Example: Cache Control on EntityManager Operation
This time we’re going to demonstrate cache control for an entity manager operation.
We’re starting by persisting two book entities, we’re purging the whole cache afterwards and we make sure, that the cache does not contain the first persisted entity.
Afterwards we’re searching for the entity adding a property to the entity manager that forces an update for this entity in the cache.
As we can see, the cache now contains a reference to the entity, so we’re resetting our playing area purging the whole cache again.
We’re running the same search as before but this time we’re feeding the entity manager with a property to bypass the cache.
Finally we’re able to see that the cache has been bypassed as expected and contains no reference to our entity.
@Test
public void shouldCacheQuery() throws Exception {
tx.begin();
Book book1 = new Book(3L, "Book 1");
Book book2 = new Book(4L, "Book 2");
em.persist(book1);
em.persist(book2);
tx.commit();
Cache cache = emf.getCache();
cache.evictAll(); // clear the whole cache
assertThat(cache.contains(Book.class, 3L), is(false)); // nothing in the
// cache
Map<String, Object> props = new HashMap<String, Object>();
props.put("javax.persistence.cache.storeMode", "REFRESH");
em.find(Book.class, 3L, props);
assertThat(cache.contains(Book.class, 3L), is(true)); // now a cache
// entry
cache.evictAll(); // clear cache
assertThat(cache.contains(Book.class, 3L), is(false));
props.put("javax.persistence.cache.storeMode", "BYPASS"); // no cache
// entry
// written
em.find(Book.class, 3L, props);
assertThat(cache.contains(Book.class, 3L), is(false)); // no cache entry
// as expected
}
Cleaning the Cache
As we can see in the example above, we have three ways to clear the cache:
-
cache.evict(EntityClass.class, primaryKey): Deletes a particular entity from the cache
-
cache.evict(EntityClass.class): Deletes all instances of a particular class and its subclasses from the cache
-
cache.evictAll: Purges the complete second-level-cache
Tutorial Sources
Please feel free to download the tutorial sources from my GitHub repository, fork it there or clone it using Git:
git clone https://github.com/hascode/jpa-caching-tutorial.git
Running the Examples
Simply run the test cases using the JUnit Runner or using Maven and the following command:
mvn test
Other JPA Articles
Please feel free to have a look at my other articles about the Java Persistence API:
Resources
Article Updates
-
2015-03-03: Links to my other JPA articles added.