Caching entities

The section shows you how to do cluster your entities using EJB 3.0 for JBoss

Caching primer

To avoid roundtrips to the database, you can use a cache for your entities. EJB 3.0 uses Hibernate which has support for a second-level cache. The Hibernate setup used for our EJB 3.0 implementation uses JBoss Cache as its underlying cache implementation. The cache itself is configured in $JBOSS_HOME/server/all/deploy/ejb3-entity-cache-service.xml. It contains a lot of settings, which can be found in the JBoss Cache documentation. The important thing to note at this stage is that the name of the cache is jboss.cache:service=EJB3EntityTreeCache.

With caching enabled:

JBoss Cache allows you to specify timeouts to cached entities. Entities not accessed within a certain amount of time are dropped from the cache in order to save memory.

Furthermore JBoss Cache supports clustering. If running within a cluster, and the cache is updated, changes to the entries in one node will be replicated to the corresponding entries in the other nodes in the cluster.

Enabling caching and choosing cache

Take a look at persistence.xml which sets up the caching for this deployment.

This defines that caching should use the JBoss Cache (which is distributed and transactional), instead of the default HashtableCacheProvider:

      <property name="hibernate.cache.provider_class" value="org.jboss.ejb3.entity.TreeCacheProviderHook"/>

This defines the object name of the JBoss Cache to be used, and references jboss.cache:service=EJB3EntityTreeCache:

      <property name="hibernate.treecache.mbean.object_name" value="jboss.cache:service=EJB3EntityTreeCache"/>

Entities

You define your entities (Customer.java and Contact.java the normal way.

The default behaviour is to not cache anything, even with the settings shown above. A very simplified rule of thumb is that you will typically want to do caching for objects that rarely change, and which are frequently read.

We also annotate the classes with the @Cache annotation to indicate that the values should be cached

   @Entity
   @Cache (usage=CacheConcurrencyStrategy.TRANSACTIONAL)
   public class Customer implements java.io.Serializable
   {
      ...
   }

   @Entity
   @Cache (usage=CacheConcurrencyStrategy.TRANSACTIONAL)
   public class Customer implements java.io.Serializable
   {
      ...
   }

This defines that Customer and Contact should be cached. Any attempt to look up Customer or Contact by their primary key, will first attempt to read the entry from the cache. If it cannot be found it will load it up from the database.

You can also cache relationship collections. Any attempt to access the contacts collection of Customer will attempt to load the data from the cache before hitting the database:

   @Entity
   @Cache (usage=CacheConcurrencyStrategy.TRANSACTIONAL)
   public class Customer implements java.io.Serializable
   {
      ...
      @Cache (usage=CacheConcurrencyStrategy.TRANSACTIONAL)
      @OneToMany(mappedBy="customer", fetch=FetchType.EAGER, cascade=CascadeType.ALL)
      public Set<Contact> getContacts()
      {
         return contacts;
      }
      ...
   }

Please consult the Hibernate documentation for further information aboout settings for caching.

Client

Open CachedEntityRun.java. It takes two arguments, they are the server:port of the two nodes to use. If you look at the 'run' target of build.xml you will see that they both default to localhost:1099, so in this case node1 and node2 will be the same, i.e. no clustering takes place.

Building and Running non-clustered

To build and run the example, make sure you have ejb3.deployer installed in JBoss 4.0.x and have JBoss running. See the reference manual on how to install EJB 3.0.
Unix:    $ export JBOSS_HOME=<where your jboss 4.0 distribution is>
Windows: $ set JBOSS_HOME=<where your jboss 4.0 distribution is>
$ ant
$ ant run
...
run:
     [java] Saving customer to node 1
     [java] Looking for customer in node 2's cache
     [java] Found customer 1 in node2 cache
     [java] Found contacts collection for 1 in node2 cache
     [java] Found contact 2 collection in node2 cache
     [java] Found contact 1 collection in node2 cache
     [java] Lookup of customer from node2 worked (via cache)
     [java] Customer: id=1; name=JBoss
     [java]     Contact: id=2; name=Bill
     [java]     Contact: id=1; name=Kabir

Now open up the JMX console and enter the jboss.cache:service=EJB3EntityTreeCache MBean. Scroll down the page untill you find the operation printDetails and press its Invoke button. This dumps out all the entries in the cache:

//org/jboss/ejb3/tutorial/clusteredentity/bean/Customer/contacts
/org.jboss.ejb3.tutorial.clusteredentity.bean.Customer.contacts#1
item: CollectionCacheEntry[1,2]
This represents the cache for the collection/relationship Customer.contacts. The collection cache only contains the ids of the entities represented. In this case it says that The Customer with id=1 points to the Contacts with id=1 and id=2.

//org/jboss/ejb3/tutorial/clusteredentity/bean/Customer
/org.jboss.ejb3.tutorial.clusteredentity.bean.Customer#1
item: CacheEntry(org.jboss.ejb3.tutorial.clusteredentity.bean.Customer)[JBoss,1]
This shows the entry for the Customer with id=1.

//org/jboss/ejb3/tutorial/clusteredentity/bean/Contact
/org.jboss.ejb3.tutorial.clusteredentity.bean.Contact#1
item: CacheEntry(org.jboss.ejb3.tutorial.clusteredentity.bean.Contact)[Kabir,1111,1]

/org.jboss.ejb3.tutorial.clusteredentity.bean.Contact#2
item: CacheEntry(org.jboss.ejb3.tutorial.clusteredentity.bean.Contact)[Bill,2222,1]
And finally we have the entries for Contact with id=1 and id=2.

Since the cache is clustered any changes to the cache in one node will be replicated to the cache of the other nodes. Let's take a look at doing this in practice.

Running clustered

This example assumes that you have two NICs on your computer, with the IP addresses 192.168.1.1 and 192.168.1.2. If you have only one network card (sorry, I only know how to do this on windows!), you can fudge this up by entering 192.168.1.1 as the IP address in the General tab of 'Internet Protocols (TCP/IP) Properties' dialog for your network settings. Next you can click 'Advanced...' and add 192.168.1.2.

If JBoss is still running take it down. Create a new server configuration by copying $JBOSS_HOME/server/all to $JBOSS_HOME/server/test. Make sure that tutorial.jar exists under both $JBOSS_HOME/server/all/deploy to $JBOSS_HOME/server/test/deploy.

Now, open up two new console windows. Go to $JBOSS_HOME/bin in both. In the first console enter

$ run.sh -c all -b 192.168.1.1

In the first console enter

$ run.sh -c test -b 192.168.1.2

(If you are using Windows, and not using cygwin, you should substitute run.sh with run.bat)

Now you should have two jboss instances running. The first is using the 'all' configuration and is bound to the IP address 192.168.1.1, the other is running the 'test' configuration and is bound to the address 192.168.1.2. If it all worked for you, congratulations, you now have a small cluster running on your machine.

Now edit the run target of the build.xml to be as follows:

   <target name="run" depends="ejbjar">
      <java classname="org.jboss.ejb3.tutorial.clusteredentity.client.CachedEntityRun" fork="yes" dir=".">
         <!-- node 1 -->
         <arg value="192.168.1.1:1099"/>
         <!-- node 2 -->
         <arg value="192.168.1.2:1099"/>
         <classpath refid="classpath"/>
      </java>
   </target>

Now run the client again. It will save data to node1's database via node 1. This data does not go into node2's database. Since we are using the embedded hypersonic database here, the two instances of jboss are using separate databases. The last thing the client does is to load the data from node2; since the data exists in the cache it does not load from the database (which is empty for node2). NOTE: This is just for the purposes of demonstrating that the cache is being used. Normally, you would have the nodes in the cluster use the same database, but the purpose of this tutorial is to show the use of a cache in a cluster.

You can examine the date in the cache for node1 and node2. Scroll down to the 'printDetails' operation and press 'Invoke'. Note that the data is the same.

Also, you can look at the databases for node1 and node2. Scroll down to the 'startDatabaseManager' operation and press 'Invoke'. Note that the Customer and Contact tables for node 1 contains data while for node2 they are empty.