Adding Ehcache to Openxava application
This article shows how to quickly enable Ehcache on Openxava applications and thus improving performance.
When viewing an entity and its graph, relationships are loaded. Adding a second-level cache fasten the retrieval of associated elements, since already loaded elements are retrieved from cache and not database.
Eventually this page explains how minuteproject treats this aspect with keeping its promise: write nothing.
As an example, we will take the Lazuly minuteproject showcase.
Openxava-Ehcache integration
In Openxava, you describe your model in the manner of Java annotated POJO.The annotations come from the standard JPA2 ORM and Openxava specific ones.
But nothing prevents you to add others. This is what is done to add caching. There are also couple of configurations to undertake to enable caching.
List of actions
- Add ehcache.xml config file at the root of your sources
- Modify persistence.xml to include second level cache
- Add caching annotation (alongside JPA2)
Remark:
Openxava comes with the ehcache.jar so there is no need to add a dependency.
Detailed actions
Add ehcache.xml
In /persistence place ehcache.xml file
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 | <ehcache> <defaultCache maxElementsInMemory= "1000" eternal= "false" timeToIdleSeconds= "300" timeToLiveSeconds= "300" overflowToDisk= "false" diskPersistent= "false" diskExpiryThreadIntervalSeconds= "300" memoryStoreEvictionPolicy= "LRU" /> <cache name= "your.domain.object" maxElementsInMemory= "5000" eternal= "false" timeToIdleSeconds= "300" timeToLiveSeconds= "600" overflowToDisk= "false" /> </ehcache> |
Modify persistence.xml
Persistence.xml file contains information related to the persitence unit such as connection pool info,
class or configuration to load. ‘persistence.xml’ is located in /persistence/META-INF
We will append properties for L2 cache.
1 2 3 4 5 6 7 8 9 | < properties > < property name = "hibernate.dialect" value = "org.hibernate.dialect.MySQLDialect" /> < property name = "hibernate.cache.provider_class" value = "net.sf.ehcache.hibernate.SingletonEhCacheProvider" /> < property name = "net.sf.ehcache.configurationResourceName" value = "/ehcache.xml" /> < property name = "hibernate.cache.use_query_cache" value = "true" /> < property name = "hibernate.cache.use_second_level_cache" value = "true" /> < property name = "hibernate.generate_statistics" value = "true" /> </ properties > |
Add cache annotation
Here the hibernate annotation is used instead of the standard one (Cacheable in fact seems not to work)
Place Cache annotation at class level of your domain object.
1 | @org .hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE) |
Example
Lazuly application
Lazuly is a sample database holding conference information used for MinuteProject showcase purpose.
Minuteproject generates a comprehensive set of artefacts to speedup the release of OX application.
Further information can be found in Minuteproject 4 Openxava Lazuly showcase.
On this part we focus on the artefacts generated for the caching specific.
Minuteproject for the generation base itself on a configuration file, where we define the datamodel to reverse engineer. In this configuration there is an enrichement part where you can add information.
One of this information deals with the type of content is held in an entity. There are 4 possibilities (reference-data, master-data, pseudo-static-data, live-business-data)
If you enrich your entity with the content-type=”master-data” or “reference-data” MinuteProject 4 Openxava will generate associated caching.
This is done here for the entity Country.
1 | < entity name = "COUNTRY" content-type = "reference-data" > |
Here are the cache related artefacts
ehcache.xml
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | < ehcache > <!-- Sets the path to the directory where cache files are created. If the path is a Java System Property it is replaced by its value in the running VM. The following properties are translated: * user.home - User's home directory * user.dir - User's current working directory * java.io.tmpdir - Default temp file path Subdirectories can be specified below the property e.g. java.io.tmpdir/one --> <!--MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @ehcache-main-config-conference@--> < diskStore path = "java.io.tmpdir" /> <!-- Mandatory Default Cache configuration. These settings will be applied to caches created programmtically using CacheManager.add(String cacheName) --> < defaultCache maxElementsInMemory = "1000" eternal = "false" timeToIdleSeconds = "300" timeToLiveSeconds = "300" overflowToDisk = "false" diskPersistent = "false" diskExpiryThreadIntervalSeconds = "300" memoryStoreEvictionPolicy = "LRU" /> <!-- The unnamed query cache --> < cache name = "org.hibernate.cache.StandardQueryCache" maxElementsInMemory = "1000" eternal = "false" timeToLiveSeconds = "300" overflowToDisk = "false" /> <!--MP-MANAGED-UPDATABLE-ENDING--> <!--MP-MANAGED-UPDATABLE-BEGINNING-DISABLE @cache-entity-country-conference@--> < cache name = "net.sf.mp.demo.conference.domain.admin.Country" maxElementsInMemory = "5000" eternal = "false" timeToIdleSeconds = "300" timeToLiveSeconds = "600" overflowToDisk = "false" /> <!--MP-MANAGED-UPDATABLE-ENDING--> <!--MP-MANAGED-ADDED-AREA-BEGINNING @custom-cache-definition@--> <!--MP-MANAGED-ADDED-AREA-ENDING @custom-cache-definition@--> </ ehcache > |
Persistence.xml
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | xsi:schemaLocation = "http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version = "1.0" > <!-- Tomcat + Hypersonic --> < persistence-unit name = "default" > < non-jta-data-source >java:comp/env/jdbc/conferenceDS</ non-jta-data-source > < class >org.openxava.session.GalleryImage</ class > < properties > < property name = "hibernate.dialect" value = "org.hibernate.dialect.MySQLDialect" /> < property name = "hibernate.cache.provider_class" value = "net.sf.ehcache.hibernate.SingletonEhCacheProvider" /> < property name = "net.sf.ehcache.configurationResourceName" value = "/ehcache.xml" /> < property name = "hibernate.cache.use_query_cache" value = "true" /> < property name = "hibernate.cache.use_second_level_cache" value = "true" /> < property name = "hibernate.generate_statistics" value = "true" /> <!--MP-MANAGED-ADDED-AREA-BEGINNING @properties@--> <!--MP-MANAGED-ADDED-AREA-ENDING @properties@--> </ properties > <!--MP-MANAGED-ADDED-AREA-BEGINNING @persistence-unit@--> <!--MP-MANAGED-ADDED-AREA-ENDING @persistence-unit@--> </ persistence-unit > <!--MP-MANAGED-ADDED-AREA-BEGINNING @persistence@--> <!--MP-MANAGED-ADDED-AREA-ENDING @persistence@--> </ persistence > |
Class annotation
1 2 3 4 5 6 7 8 9 | @org .hibernate.annotations.Cache(usage = org.hibernate.annotations.CacheConcurrencyStrategy.READ_WRITE) //MP-MANAGED-ADDED-AREA-BEGINNING @class-annotation@ //MP-MANAGED-ADDED-AREA-ENDING @class-annotation@ public class Country { @Hidden @Id @Column (name= "id" ) @GeneratedValue (strategy = GenerationType.AUTO) private Integer id; ... |
Generated code remark
The generated code has markers inside file extension comment.
Within MP-MANAGED-ADDED-AREA-BEGINNING and MP-MANAGED-ADDED-AREA-ENDING you can place customized code
Within MP-MANAGED-UPDATABLE-BEGINNING-DISABLE and MP-MANAGED-UPDATABLE-ENDING you can alter the code. To keep your modifications please change MP-MANAGED-UPDATABLE-BEGINNING-DISABLE into MP-MANAGED-UPDATABLE-BEGINNING-ENABLE.
Updatable code prevent you to lose your customisation over consecutive generations.
For more information on updatable code see Minuteproject updatable code.
Generation
- Place the following file mp-config-LAZULY-OPENXAVA.xml in /mywork/config
- on a prompt execute mp-model-generation(.sh/cmd) mp-config-LAZULY-OPENXAVA.xml
- the resulting artefacts in /DEV/output/openxava/conference
To generate use the updated version of mp-config-LAZULY-OPENXAVA.xml
001 002 003 004 005 006 007 008 009 010 011 012 013 014 015 016 017 018 019 020 021 022 023 024 025 026 027 028 029 030 031 032 033 034 035 036 037 038 039 040 041 042 043 044 045 046 047 048 049 050 051 052 053 054 055 056 057 058 059 060 061 062 063 064 065 066 067 068 069 070 071 072 073 074 075 076 077 078 079 080 081 082 083 084 085 086 087 088 089 090 091 092 093 094 095 096 097 098 099 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 | <! DOCTYPE root> < generator-config > < configuration > < conventions > < target-convention type = "enable-updatable-code-feature" /> </ conventions > < model name = "conference" version = "1.0" package-root = "net.sf.mp.demo" > < data-model > < driver name = "mysql" version = "5.1.16" groupId = "mysql" artifactId = "mysql-connector-java" ></ driver > < dataSource > < driverClassName >org.gjt.mm.mysql.Driver</ driverClassName > < url >jdbc:mysql://127.0.0.1:3306/conference</ url > < username >root</ username > < password >mysql</ password > </ dataSource > <!-- for Oracle and DB2 please set the schema <schema> </schema> --> < primaryKeyPolicy oneGlobal = "true" > < primaryKeyPolicyPattern name = "autoincrementPattern" ></ primaryKeyPolicyPattern > </ primaryKeyPolicy > </ data-model > < business-model > <!-- <generation-condition> <condition type="exclude" startsWith="DUAL"></condition> </generation-condition> --> < business-package default = "conference" > < condition type = "package" startsWith = "STAT" result = "statistics" ></ condition > < condition type = "package" startsWith = "COUNTRY" result = "admin" ></ condition > < condition type = "package" startsWith = "ROLE" result = "admin" ></ condition > </ business-package > < enrichment > < conventions > < column-naming-convention type = "apply-strip-column-name-suffix" pattern-to-strip = "_ID" /> < reference-naming-convention type = "apply-referenced-alias-when-no-ambiguity" is-to-plurialize = "true" /> </ conventions > < entity name = "COUNTRY" content-type = "reference-data" > < semantic-reference > < sql-path path = "NAME" /> </ semantic-reference > </ entity > < entity name = "CONFERENCE_MEMBER" > < semantic-reference > < sql-path path = "FIRST_NAME" /> < sql-path path = "LAST_NAME" /> </ semantic-reference > < field name = "STATUS" > < property tag = "checkconstraint" alias = "conference_member_status" > < property name = "PENDING" value = "PENDING" /> < property name = "ACTIVE" value = "ACTIVE" /> </ property > </ field > < field name = "EMAIL" > < stereotype stereotype = "EMAIL" /> </ field > </ entity > < entity name = "SPEAKER" > < field name = "BIO" > < stereotype stereotype = "HTML_TEXT" /> </ field > < field name = "PHOTO" > < stereotype stereotype = "PHOTO" /> </ field > < field name = "WEB_SITE_URL" > < stereotype stereotype = "WEBURL" /> </ field > </ entity > < entity name = "PRESENTATION" > < field name = "STATUS" > < property tag = "checkconstraint" alias = "presentation_status" > < property name = "PROPOSAL" value = "PROPOSAL" /> < property name = "ACTIVE" value = "ACTIVE" /> </ property > </ field > </ entity > < entity name = "SPONSOR" > < field name = "STATUS" > < property tag = "checkconstraint" alias = "sponsor_status" > < property name = "PENDING" value = "PENDING" /> < property name = "ACTIVE" value = "ACTIVE" /> </ property > </ field > < field name = "PRIVILEGE_TYPE" > < property tag = "checkconstraint" alias = "sponsor_privilege" > < property name = "GOLDEN" value = "Golden" /> < property name = "SILVER" value = "Silver" /> < property name = "BRONZE" value = "Bronze" /> </ property > </ field > </ entity > <!-- views --> < entity name = "stat_mb_per_ctry_conf" alias = "MEMBER_PER_COUNTRY_AND_CONFERENCE" > < virtual-primary-key isRealPrimaryKey = "true" > < property name = "virtualPrimaryKey" value = "ID" /> </ virtual-primary-key > </ entity > < entity name = "stat_mb_by_role" alias = "MEMBER_PER_ROLE_COUNTRY_AND_CONFERENCE" > < virtual-primary-key isRealPrimaryKey = "true" > < property name = "virtualPrimaryKey" value = "id" /> </ virtual-primary-key > < field name = "stat_mb_per_ctry_conf_ID" linkToTargetEntity = "stat_mb_per_ctry_conf" linkToTargetField = "id" ></ field > </ entity > </ enrichment > </ business-model > </ model > < targets > <!-- openxava --> < target refname = "OpenXava" name = "OpenXava" fileName = "mp-template-config-openxava-last-features.xml" outputdir-root = "../../DEV/output/openxava/conference" templatedir-root = "../../template/framework/openxava" > </ target > < target refname = "JPA2-LIB" fileName = "mp-template-config-JPA2-LIB.xml" templatedir-root = "../../template/framework/jpa" > </ target > < target refname = "BSLA-LIB" fileName = "mp-template-config-bsla-LIB-features.xml" templatedir-root = "../../template/framework/bsla" > </ target > < target refname = "CACHE-LIB" fileName = "mp-template-config-CACHE-LIB.xml" templatedir-root = "../../template/framework/cache" > </ target > </ targets > </ configuration > </ generator-config > |
Test
To ensure that the caching is working properly:
- Enable hibernate logging. Add the following snippet as extra properties in persistence.xml.
1 2 | < property name = "hibernate.show_sql" value = "true" /> < property name = "hibernate.format_sql" value = "true" /> |
- navigate to an entity that reference country (example Address)
- When you view the detail of this entity you will notice that there is a load of the associated entity ‘country’
- But the second time you access to the details of this entity (or another entity referencing the same country instance), the country is not loaded twice from the database.
Reference: Adding Ehcache to Openxava application from our JCG partner Florian Adler at the minuteproject blog.