There has been excellent coverage on how to attain the most performance out of your Seam-based Web applications. One aspect of improving Seam performance that, in my opinion, has received less attention, is the framework’s excellent built-in support for caching of entity java beans and query results.
In this article, I will briefly describe the steps I took in order to configure some basic to intermediate-level caching throughout our system. Although I did not go as far as to gather metrics on the performance gain, I will say that our entire team noticed a 30-50% improvement across the board – and all from some fairly easy work.
Because of our non-JBoss native application stack – we’re using WebLogic 11g – I chose to select EhCache as my cache provider of choice.
Package EhCache
For this primer we will be using EhCache 1.5. Download ehcache-1.5.0.jar (link) and place it in your /lib directory. Using your chosen build script (Ant, Maven, etc.), ensure that ehcache-1.5.0.jar is built to your EAR so that the classpath loader will find the library at startup.
Configure the Provider
Next, we need to make sure that EhCache (or your chosen cache provider) is properly configured. Modify components.xml and tell Seam to load the EhCache provider.
components.xml
<cache:eh-cache-provider name="cacheProvider"/>
The last step is to configure your persistence provider. Normally with Seam this configuration is broken up into several profiles (local, dev, test, and prod). In this example, I will be showing how I configured my persistence provider for our “local” profile.
Note that there are several options for hibernate.cache.provider_class; I have chosen to use the Singleton provider in this example.
persistence-local.xml
<!-- ... other properties .... ->
<property name="hibernate.cache.use_query_cache" value="true"/>
<property name="hibernate.use_second_level_cache" value="true"/>
<property name="hibernate.default_batch_fetch_size" value="16"/>
<property name="hibernate.generate_statistics" value="true"/>
<property name="hibernate.cache.provider_class" value="net.sf.ehcache.hibernate.SingletonEhCacheProvider"/>
<property name="net.sf.ehcache.configurationResourceName" value="/ehcache2.xml"/>
Note in the example above that we have set hibernate.generate_statistics equal to true; this will initially help you tune your caching provider by enabling you to compare the query statistics (once properly setup via your log4j configuration).
Annotate Your Entities
Be aware that Seam comes with built-in caching; you can always annotate your entity java beans with the @Cache annotation. However, in this primer, we are building a path towards a more robust and advanced cache configuration. Even though the annotations remain the same, how they will be interpreted and used by EhCache will be up to us.
Annotating your entities are a simple and effective means to provide high-level caching of your application’s data. The three main cache strategies that we will concern ourselves with are:
READ_WRITE – should be used for objects that will be read and written to; these objects will change
READ_ONLY – should be used in cases where the object does not change; this tells Hibernate and EhCache to strongly cache this object
NONSTRICT_READ_WRITE – should be used in cases where an object is rarely written to; it will be cached to a degree less than READ but more than READ_WRITE
TRANSACTIONAL – this strategy is not available with EhCache but can be used with other cache providers
In my first entity, I have an object (User.java) that is both read and written; I want it to be cached, but it will need to be designated as READ_WRITE. User objects are heavily queried throughout our system but often modified. Later, I will show how you can provide more control over the caching of a specific object or region.
@Entity @Audited
@Table(name="USERS")
@Cache(usage=CacheConcurrencyStrategy.READ_WRITE
@Name("user")
@Scope(ScopeType.EVENT)
@BypassInterceptors
public class User implements Serializable
{
/** the users internal id **/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
/** the userid/logon **/
@UserPrincipal
@Length(min=5, max=32)
private String userId;
// Rest of class below...
In my second entity I have an object called AdminValue.java; this object is used as metadata throughout the system. This object populates dropdowns and for the most part will not change.
@Entity @Audited
@Table(name = "APP_ADMIN_VALUES")
@Cache(usage=CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
@Name("adminValue")
@Scope(ScopeType.EVENT)
public class AdminValue implements Serializable
{
/** the id **/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name="ID")
private Long id;
/**
* name of the value
*/
@NotNull
@Length(max=50)
@Index(name="IDX_ADMIN_VALUE_NAME")
private String name;
// Rest of class below...
Next, we have an object (MenuItem.java) that truly does not change. It is populated when the application deploys and will rarely, if ever, change. Because it is used throughout interface, though, I want it to be cached heavily to speed up page rendering.
@Entity
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region = "com.bah.englink.MenuItem")
@Name("menuItem")
@Scope(ScopeType.EVENT)
@Table(name="APP_MENU_ITEMS")
public class MenuItem implements Serializable
{
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id; //Uniquie id for the menu item
@ManyToOne(optional=true)
@Index(name="IDX_MENU_ITEM_FK01")
private MenuItem parent; //Defines the parent associated with item, can be null for the top level
@NotNull
@Length(max=64)
@Index(name="IDX_MENU_ITEM_NAME")
private String name; //Name that displays on the screen
// Rest of class below...
Using Query Hints with Named Queries
Named queries are particularly useful; they are strongly typed, and are therefore checked at compile-time, and cached via Seam/Hibernate JPA. You can provide further caching of named queries like so:
@NamedQueries({
@NamedQuery(
name="lookupCategoryByName",
query="from AdminCategory ac where ac.name=:name order by ac.name asc",
hints={@QueryHint(name="org.hibernate.cacheable", value="true")}),
@NamedQuery(
name="lookupCategoryByNameByModule",
query="from AdminCategory ac where ac.name=:categoryName and ac.adminModule.name=:moduleName",
hints={@QueryHint(name="org.hibernate.cacheable", value="true")})
})
public class AdminCategory implements Serializable
{
Advanced Configuration Via Regions
Here is where the fun begins; using the ehcache2.xml that we specified in our persistence provider, we can define minute details regarding how a particular object (or region of objects) are cached. This feature provides a high-level of control. There are many aspects of these configuration options that are a bit above my head, so I’ve outlined some examples that I used to show what is possible.
Whenever specifying a region with your @Cache annotation, you should try to have a corresponding region defined in your ehcache2.xml. Here are some examples.
//@Cache(usage=CacheConcurrencyStrategy.READ_WRITE, region = "com.bah.englink.ejb.User")
<cache name="com.bah.englink.ejb.User"
maxElementsInMemory="10000"
maxElementsOnDisk="20000"
eternal="false"
overflowToDisk="true"
diskSpoolBufferSizeMB="100"
timeToIdleSeconds="4000"
timeToLiveSeconds="8000"
memoryStoreEvictionPolicy="LRU"
/>
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY, region = "com.bah.englink.MenuItem")
<cache name="com.bah.englink.MenuItem"
maxElementsInMemory="10000"
maxElementsOnDisk="5000"
eternal="false"
overflowToDisk="true"
diskSpoolBufferSizeMB="40"
timeToIdleSeconds="9000"
timeToLiveSeconds="18000"
memoryStoreEvictionPolicy="LFU"
/>
Note, the bundled ehcache2.xml that comes with ehcache-1.5.0.jar has many more examples for you to review and understand.
Logging
If you would like to see exactly which objects EhCache is storing, and when/if cache objects are being hit, you can turn on debug output from EhCache via your log4j.xml settings.
<category name="org.hibernate.cache">
<priority value="DEBUG"/>
</category>
[/sourecode]
After doing so, you should see cache output, such as in the example below.
[sourcecode language='text']
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-90
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-66
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-65
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-51
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-89
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-82
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-88
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-1004
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-1005
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-14
15:42:44,107 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-83
15:42:44,123 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-26
15:42:44,123 DEBUG [ReadOnlyCache] Caching: com.bah.englink.MenuItem#-64
Conclusion
I hope this has been a useful, if intermediate, review of caching in your Seam-based application using Hibernate and EhCache. Please feel free to let me know what I have missed or if you find any mistakes.
More Resources
Hibernate: Truly Understanding Second-Level Cache
Second-Level Cache with EhCache
Look Who\'s Talking