In my last post I explained how I'd like to implement a persistence unit as a self-contained module that I could drop into another application that already has its own persistence unit. Both persistence units should talk to their respective database schemas without problems.
The two basic parts to the solution I worked out are: (1) ensure that the location of all configuration files and the names of all Spring-managed beans are unique; and (2) always specify which persistence unit and transaction manager a class requires.
To implement the solution, I established a set of conventions based on the persistence unit name:
First convention. A reusable code module that contains a persistence unit should have three configuration files located in META-INF/persistence-unit-name
. If the persistence unit is named myunit
, here is the structure of the jar file that contains the module:
com/**/**/*.class
(Java classes related to the persistence unitMETA-INF
myunit
META-INF/myunit/applicationContext.xml
META-INF/myunit/database.properties
META-INF/myunit/persistence.xml
The persistence.xml
only has to specify the persistence unit name:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0"> <persistence-unit name="myunit" transaction-type="RESOURCE_LOCAL"> </persistence-unit> </persistence>
The database.properties
file contains a set of overridable connection settings:
database.url=jdbc:oracle:thin:@localhost:1521:ORCL database.user=mydata database.password=mydata
Second convention. The Spring configuration file for the reusable module should define a database connection pool, entity manager factory and transaction manager for the module's persistence unit. To avoid naming conflicts, all bean names and properties should be prefixed with the persistence unit name:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd"> <context:annotation-config /> <context:component-scan base-package="com.mycompany.myunit" /> <bean id="myunitDatabaseConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <value>classpath:/META-INF/myunit/database.properties</value> </property> <property name="systemPropertiesModeName"> <value>SYSTEM_PROPERTIES_MODE_OVERRIDE</value> </property> <property name="placeholderPrefix" value="$MYUNIT{"/> </bean> <bean id="myunitDataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="oracle.jdbc.driver.OracleDriver" /> <property name="jdbcUrl" value="$MYUNIT{database.url}" /> <property name="user" value="$MYUNIT{database.user}" /> <property name="password" value="$MYUNIT{database.password}" /> </bean> <bean id="myunitEntityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="myunitDataSource" /> <property name="jpaVendorAdapter" ref="myunitJpaAdapter" /> <property name="persistenceXmlLocation" value="classpath:/META-INF/myunit/persistence.xml"/> </bean> <bean id="myunitTransactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="myunitEntityManagerFactory" /> </bean> <tx:advice id="myunitTxAdvice" transaction-manager="myunitTransactionManager"> <tx:attributes> <tx:method name="*" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:pointcut id="myunitTxPointcut" expression="execution(* com.mycompany.mypackage.*.*(..))"/> <aop:advisor advice-ref="myunitTxAdvice" pointcut-ref="myunitTxPointcut"/> </aop:config> <bean id="myunitJpaAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="database" value="ORACLE" /> <property name="showSql" value="true" /> </bean> </beans>
Third convention. Always specify the persistence unit name when using the PersistenceContext
annotation to inject an entity manager into a class that is part of the module:
@PersistenceContext(unitName = "myunit") private EntityManager entityManager;
Fourth convention. Use explict AOP configuration in the Spring configuration file for declaring transaction boundaries. The Transactional
annotation does not support multiple local transaction managers tied to different persistence units, so it cannot be used in the context of a reusable module.
Note that the module should not publicly expose any non-transactional methods involving database access. This ensures that the correct transaction manager is used for the persistence unit.
With these conventions, I was able to build the module as a jar file that can be tested in isolation and also deployed to other applications in three steps:
- add the jar file to the classpath
- add
classpath:/META-INF/myunit/applicationContext.xml
to the list of configuration locations for the application's Spring container. - (optional) override database settings via system properties as necessary
The conventions may be too restrictive in some cases but they work well for my situation.
2 comments:
I'd like to thank you for this pretty good solution. That is exactly what I was looking for.
Actually, you can still use the @Transactional annotation when you have multiple transaction managers. Simply specify the name of the transaction manager to use with the value attribute.
Example: @Transactional(value="theOtherOne")
Post a Comment