hibernate

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.
 
 
 
 
 
 

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>

Fig 1 : Project Structure
Fig 1 : Project Structure

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_cacheproperty 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.

Tip
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.

Tip
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.

Download
You can download the source code of this example here: SpringDataNeo4JExample.zip

Chandan Singh

Chandan holds a degree in Computer Engineering and is a passionate software programmer. He has good experience in Java/J2EE Web-Application development for Banking and E-Commerce Domains.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Inline Feedbacks
View all comments
Back to top button