Second Level Cache in Hibernate Example
In one of the previous examples, we explained how we can configure Spring Data with JPA using Hibernate as the JPA as the underlying vendor.
In this example, we will demonstrate how we can use Second Level Cache in Hibernate to optimize application performance and also avoid common pitfalls.
Table Of Contents
1. What is Second Level Cache?
Every Hibernate Session has a cache associated with it which is also referred to as First Level Cache. However, this cache expires/invalidates once the Session
is closed.
Second Level Cache is associated with the SessionFactory
and outlasts the First Level Cache. When the user fetches the data from the database for the first time, the data gets stored in the Second Level Cache if it is enabled for that entity. Thereafter, whenever the user requests the data from the second level cache is returned, thus saving network traffic and a database hit.
Hibernate also supports Query Cache, which stores the Data returned by a Hibernate Query
.
Data is stored in the cache in the form of key value pairs of String. The key being the unique identifier/Primary key of the table in case of second level Entity Cache or the Query String and parameter values in case of Query Cache. The value is the data associated with that particular Primary Key or the Query. Let’s start with the project set-up.
2. Project Set-Up
For our example, we shall be using Ehcache as our Cache provider. We shall use Maven
to setup our project. Open Eclipse and create a simple Maven project and check the skip archetype selection checkbox on the dialogue box that appears. Replace the content of the existing pom.xml
with the one provided below:
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.jcg.examples</groupId> <artifactId>Hibernate-Secondary-Cache</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Hibernate Secondary Cache Example</name> <description>Hibernate Secondary Cache Example</description> <dependencies> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>5.0.0.Final</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.37</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId> <version>5.0.0.Final</version> </dependency> <dependency> <groupId>net.sf.ehcache</groupId> <artifactId>ehcache-core</artifactId> <version>2.6.11</version> </dependency> </dependencies> </project>
This will import all the required dependency for our project. This completes the project setup and we can start with the actual Implementation.
3. Implementation
Let us start with the creation of entities.
Account.java
package com.examples.jcg.entity; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Table(name = "account", catalog = "test") @Cache(usage=CacheConcurrencyStrategy.READ_WRITE, region="account") public class Account implements java.io.Serializable { /** * */ private static final long serialVersionUID = -2876316197910860162L; private long accountNumber; private Person person; private String accountType; public Account() { } public Account(long accountNumber) { this.accountNumber = accountNumber; } public Account(long accountNumber, Person person, String accountType) { this.accountNumber = accountNumber; this.person = person; this.accountType = accountType; } @Id @Column(name = "Account_Number", unique = true, nullable = false) public long getAccountNumber() { return this.accountNumber; } public void setAccountNumber(long accountNumber) { this.accountNumber = accountNumber; } @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "Person_id") public Person getPerson() { return this.person; } public void setPerson(Person person) { this.person = person; } @Column(name = "Account_Type", length = 45) public String getAccountType() { return this.accountType; } public void setAccountType(String accountType) { this.accountType = accountType; } @Override public String toString() { return "Account [accountNumber=" + accountNumber + ", person=" + person + ", accountType=" + accountType + "]"; } }
Person.java
package com.examples.jcg.entity; import java.util.HashSet; import java.util.Set; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import static javax.persistence.GenerationType.IDENTITY; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.Table; import org.hibernate.annotations.Cache; import org.hibernate.annotations.CacheConcurrencyStrategy; @Entity @Table(name = "person", catalog = "test") @Cache(usage=CacheConcurrencyStrategy.READ_WRITE, region="person") public class Person implements java.io.Serializable { /** * */ private static final long serialVersionUID = -9035342833723545079L; private Long pid; private Double personAge; private String personName; private Set accounts = new HashSet(0); public Person() { } public Person(Double personAge, String personName, Set accounts) { this.personAge = personAge; this.personName = personName; this.accounts = accounts; } @Id @GeneratedValue(strategy = IDENTITY) @Column(name = "pId", unique = true, nullable = false) public Long getPid() { return this.pid; } public void setPid(Long pid) { this.pid = pid; } @Column(name = "personAge", precision = 22, scale = 0) public Double getPersonAge() { return this.personAge; } public void setPersonAge(Double personAge) { this.personAge = personAge; } @Column(name = "personName") public String getPersonName() { return this.personName; } public void setPersonName(String personName) { this.personName = personName; } @OneToMany(fetch = FetchType.LAZY, mappedBy = "person") public Set getAccounts() { return this.accounts; } public void setAccounts(Set accounts) { this.accounts = accounts; } @Override public String toString() { return "Person [pid=" + pid + ", personAge=" + personAge + ", personName=" + personName + "]"; } }
@Cache
is used to mark the entity as cache-able. usage
parameter tells the Hibernate which Concurrency Strategy
is to be used for that particular Entity/Collection. Concurrency Strategy refers to the act of updating the entity once it is cached once the underlying data is modified/updated. Different cache Concurrency strategies in ascending order of their strictness are :
READ_ONLY
NONSTRICT_READ_WRITE
READ_WRITE
TRANSACTIONAL
EhCache does not support Transactional Concurrency Strategy.
region
argument declares the name of the cache region in which the instances of this entity will be cached. By default it is the fully qualified name of the entity.
After Entities are done, lets start with the Hibernate Configuration :
hibernate.cfg.xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD 3.0//EN" "http://www.hibernate.org/dtd/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost/test</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">toor</property> <property name="hibernate.dialect">org.hibernate.dialect.MySQL5Dialect</property> <property name="hibernate.current_session_context_class">thread</property> <property name="hibernate.show_sql">true</property> <property name="hibernate.cache.use_second_level_cache">true</property> <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> <property name="hibernate.cache.use_query_cache">true</property> <property name="net.sf.ehcache.configurationResourceName">ehcache.xml</property> <mapping class="com.examples.jcg.entity.Person" /> <mapping class="com.examples.jcg.entity.Account" /> </session-factory> </hibernate-configuration>
To enable the Second Level Cache, we use the property hibernate.cache.use_second_level_cache
and set it to true
hibernate.cache.use_query_cache
property is used to select the underlying Cache Vendor which is EhCacheRegionFactory
in our case.
To enable the Query Cache, we use the property hibernate.cache.use_query_cache
and set it to true
.
You cannot configure Query Cache without the Second Level Cache.
Query Cache stores the Keys of the entities and not the entire Object Values. When the query cache is hit, the actual records are fetched from the Second Level Cache.
Lastly, net.sf.ehcache.configurationResourceName
is used to provide the XML
filename used to configure the Ehcache. If this file is not provided, it is picked from the ehcache-failsafe.xml
present in the ehcache-core.jar
.
ehcache.xml
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="ehcache.xsd" updateCheck="true" monitoring="autodetect" dynamicConfig="true"> <diskStore path="java.io.tmpdir/ehcache" /> <defaultCache maxEntriesLocalHeap="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" diskSpoolBufferSizeMB="30" maxEntriesLocalDisk="10000000" diskExpiryThreadIntervalSeconds="120" memoryStoreEvictionPolicy="LRU" statistics="true"> <persistence strategy="localTempSwap" /> </defaultCache> <cache name="org.hibernate.cache.internal.StandardQueryCache" maxEntriesLocalHeap="5" eternal="false" timeToLiveSeconds="120"> <persistence strategy="localTempSwap" /> </cache> <cache name="org.hibernate.cache.spi.UpdateTimestampsCache" maxEntriesLocalHeap="5000" eternal="true"> <persistence strategy="localTempSwap" /> </cache> </ehcache>
Parameters about the Ehcache
like timeToLiveSeconds
,peristence strategy
maximum number of key-value pairs per-cache-region etc. can be configured from ehcache.xml
.
That is all from setting up the Second Level/L2 Cache point of view. We can now use the Cache to cache our entities.
SecondaryCacheExample.java
package com.jcg.examples; import java.util.List; import org.hibernate.Query; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import com.examples.jcg.entity.Account; import com.examples.jcg.entity.Person; public class SecondaryCacheExample { public static void main(String[] args) { SessionFactory sessionFactory = getSessionFactory(); /***************************First Session Begins**********************************/ Session session = sessionFactory.getCurrentSession(); session.beginTransaction(); System.out.println(session.get(Person.class, 1l)); Query query = session.createQuery("from Account where accountNumber =1").setCacheable(true).setCacheRegion("account"); @SuppressWarnings("unchecked") List<Account> personList = query.list(); System.out.println(personList); session.getTransaction().commit(); //sessionFactory.getCache().evictEntity(Person.class, 1l); /***************************First Session Ends**********************************/ /***************************Second Session Begins**********************************/ Session sessionNew = sessionFactory.getCurrentSession(); sessionNew.beginTransaction(); System.out.println(sessionNew.get(Person.class, 1l)); Query anotherQuery = sessionNew.createQuery("from Account where accountNumber =1"); anotherQuery.setCacheable(true).setCacheRegion("account"); @SuppressWarnings("unchecked") List<Account> personListfromCache = anotherQuery.list(); System.out.println(personListfromCache); sessionNew.getTransaction().commit(); /***************************Second Session Ends**********************************/ sessionFactory.close(); } private static SessionFactory getSessionFactory() { return new Configuration().configure().buildSessionFactory(); } }
Here’s the output:
Hibernate: select person0_.pId as pId1_1_0_, person0_.personAge as personAg2_1_0_, person0_.personName as personNa3_1_0_ from test.person person0_ where person0_.pId=? Person [pid=1, personAge=120.0, personName=Krishna] Hibernate: select account0_.Account_Number as Account_1_0_, account0_.Account_Type as Account_2_0_, account0_.Person_id as Person_i3_0_ from test.account account0_ where account0_.Account_Number=1 [Account [accountNumber=1, person=Person [pid=1, personAge=120.0, personName=Krishna], accountType=Savings]] Person [pid=1, personAge=120.0, personName=Krishna] [Account [accountNumber=1, person=Person [pid=1, personAge=120.0, personName=Krishna], accountType=Savings]]
In the output we can see that the queries for both the selects occur only for the first time. For the second session, they are fetched from the Second Level Cache itself.
To cache the query results in the query cache, it is important to set cacheable
property of the Query
to true
. It not only caches the query, but also checks if the query is already cached.
Without setting
cacheable
property to true
, Hibernate won’t hit the Query Cache
even if the Query was previously cached.4. Pit-Falls and Common Mistakes
While the Second Level Cache does offer many advantages, it can downgrade the performance if not setup properly. Let’s look at some common mistakes.
- As I mentioned earlier, Query Cache does not store the actual entities, rather, it stores only the unique indentifier/Primary key of the Entities returned by the query. The Values are stored in the L2 Cache in the form of Key-Value pairs. Now, if the L2 Cache is set to expire earlier than the Query Cache, and Query cache hits the L2 cache for the entities, the records will be fetched from the Database and our purpose will be defeated. To avoid this, both the caches should be configured to timeout in sync.
- Another potential problem with the Query Cache again is with the Native SQL Queries. When a native SQL Query is executed via hibernate, it has no knowledge of the data being changed by the query. So rather than having the potentially corrupt Hibernate invalidates the whole L2 cache!
To avoid this, we need to specify the Cache Region or the Entity Class which will be affected by the Native Query. Here’s how to do it:
SQLQuery sqlQuery = session.createSQLQuery("Update Person set......"); sqlQuery.addSynchronizedEntityClass(Person.class);
OR
SQLQuery sqlQuery = session.createSQLQuery("Update Person set......"); sqlQuery.addSynchronizedQuerySpace("Person");
- Second Level Cache assumes that the Database is updated only through the Hibernate. Updating Database by any other means may lead to the Cache having dirty data and violate the Integrity of the Data being stored.
5. Download the source code
In this example, we studied the benefits of Second Level cache in Hibernate and how we can avoid the pitfalls to achieve maximum throughput from the application.
You can download the source code of this example here: SpringDataNeo4JExample.zip