The project I’m working on wanted to have audit tables for many of the tables in the system. Instead of writing our own plugins and creating the tables we decided to use Hibernate Envers.
Hibernate Envers is a really nice addition to Hibernate that allows you to flag domain objects with @Audited
and it will do all the work for you.
The Problem
The problem with Envers is in its configuration. Being that it is a JPA solution, it sticks to the JPA standard very tightly. Well, what’s wrong with that? Things should stick to the standard. I agree with that. The thing is (in my best Jeremy Clarkson voice) is that this means all the configuration is in the persistence.xml file.
Now, this is fine and dandy in a set environment where you know what you will be deploying to and how things will be configured. But what if you don’t? Well, the standard convention is to create an external properties file and have Spring read the properties file when it starts up and replace all of your ${} with the variables in the external properties files.
This allows things like DB connection information and such to be external to the app. The issue is that this variable replacement doesn’t exist in persistence.xml (I hear that it may be in the next spec) and that Envers does something a little odd. What it does that is odd is that it uses the connection information that it picks up from your Hibernate configuration (which you could set with ${} when you configured your dataSource in your Spring configuration) but it doesn’t use the same schema. Envers defaults to the schema of the user who logged into the db and well, that might not be the same schema you have Hibernate using or even what you want.
To address this, Envers allows you to set the schema to use with the org.hibernate.envers.default_schema
property. Great! But wait, it’s set in the persistence.xml. You know, the thing you can’t use the external properties file against. This means that you have to hard code the schema you want to use. So much for external configuration you say. Well, not really
The Solution
Luckily Spring supports registering a post-processor of the persistence.xml. The LocalContainerEntityManagerFactoryBean
allows you to set a persistenceUnitPostProcessors
property that allows you to manipulate the persistence.xml before it is really used.
So, what we can do is create an object that takes properties such as “schema” and sets the org.hibernate.envers.default_schema
property for us. Since we are doing this in Spring we can leverage its ${} replacement and set the schema from the external properties file. Woot!
First things first, lets create our object:
import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; import org.springframework.orm.jpa.persistenceunit.PersistenceUnitPostProcessor; /** * Allows for the modification of Hibernate Envers configuration via external * properties files instead of being hard-coded into the persistence.xml * <p/> * * Should be added onto org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean * * <p/> * Example: * <bean id="x" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> * <property name="dataSource" ref="dataSource"/> * <property name="jpaVendorAdapter"> * <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> * <property name="showSql" value="false"/> * <property name="databasePlatform" value="org.hibernate.dialect.SQLServerDialect"/> * <property name="generateDdl" value="true"/> * </bean> * </property> * <property name="persistenceUnitName" value="model"/> * <property name="persistenceUnitPostProcessors"> * <list> * <bean class="com.broadridge.adc.fundmap.batch.EnversPersistenceUnitPostProcessor"> * <property name="schema" value="SCHEMA_NAME"/> * </bean> * </list> * </property> * </bean> * @author seamans * */ public class EnversPersistenceUnitPostProcessor implements PersistenceUnitPostProcessor { private static final String ENVERS_SCHEMA = "org.hibernate.envers.default_schema"; private static final String ENVERS_CATALOG = "org.hibernate.envers.default_catalog"; private static final String ENVERS_TRACK_CHANGE = "org.hibernate.envers.track_entities_changed_in_revision"; private static final String ENVERS_TABLE_SUFFIX = "org.hibernate.envers.audit_table_suffix"; private static final String ENVERS_TRACK_COLLECTION_CHANGE = "org.hibernate.envers.revision_on_collection_change"; private static final String ENVERS_NO_OPT_LOCKING = "org.hibernate.envers.do_not_audit_optimistic_locking_field"; private static final String ENVERS_STORE_ON_DELETE = "org.hibernate.envers.store_data_at_delete"; private static final String ENVERS_REVISION_LISTENER = "org.hibernate.envers.revision_listener"; private String schema; private String catalog; private String trackChange; private String suffix; private String trackCollectionChange; private String noOptLocking; private String storeOnDelete; private String revisionListener; @Override public void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo pui) { if (schema != null) { pui.addProperty(ENVERS_SCHEMA, schema); } if (catalog != null) { pui.addProperty(ENVERS_CATALOG, catalog); } if (trackChange != null) { pui.addProperty(ENVERS_TRACK_CHANGE, trackChange); } if (suffix != null) { pui.addProperty(ENVERS_TABLE_SUFFIX, suffix); } if (trackCollectionChange != null) { pui.addProperty(ENVERS_TRACK_COLLECTION_CHANGE, trackCollectionChange); } if (noOptLocking != null) { pui.addProperty(ENVERS_NO_OPT_LOCKING, noOptLocking); } if (storeOnDelete != null) { pui.addProperty(ENVERS_STORE_ON_DELETE, storeOnDelete); } if (revisionListener != null) { pui.addProperty(ENVERS_REVISION_LISTENER, revisionListener); } } public void setSchema(String schema) { this.schema = schema; } public void setCatalog(String catalog) { this.catalog = catalog; } public void setTrackChange(String trackChange) { this.trackChange = trackChange; } public void setSuffix(String suffix) { this.suffix = suffix; } public void setTrackCollectionChange(String trackCollectionChange) { this.trackCollectionChange = trackCollectionChange; } public void setNoOptLocking(String noOptLocking) { this.noOptLocking = noOptLocking; } public void setStoreOnDelete(String storeOnDelete) { this.storeOnDelete = storeOnDelete; } public void setRevisionListener(String revisionListener) { this.revisionListener = revisionListener; } }
Basically, all I’ve done is write a PersistenceUnitPostProcessor
that allows us to set properties based on standard bean conventions.
Now we can leverage Spring and set the properties from an external properties file:
<bean id="entityManager" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="false"/> <property name="databasePlatform" value="org.hibernate.dialect.SQLServerDialect"/> <property name="generateDdl" value="true"/> </bean> </property> <property name="persistenceUnitName" value="model"/> <property name="persistenceUnitPostProcessors"> <list> <bean class="EnversPersistenceUnitPostProcessor"> <property name="schema" value="${dataSource.audit.schema}"/> </bean> </list> </property> </bean>
See the ${dataSource.audit.schema}
? Just put dataSource.audit.schema
in your external properties file and you will be able to control the schema name from outside the application!
You will notice that my object also supports setting other properties. I wrote code for all of the other properties that I could find in case I needed them at a later time.
That’s it! Envers can now be configures from an external property file. Enjoy!