Sunday, February 10, 2008

Multiple Persistence Units with Spring and JPA: Part II

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 unit
  • META-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:

  1. add the jar file to the classpath
  2. add classpath:/META-INF/myunit/applicationContext.xml to the list of configuration locations for the application's Spring container.
  3. (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:

Sant'Anna, Bruno said...

I'd like to thank you for this pretty good solution. That is exactly what I was looking for.

QDog said...

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")