Spring 3.1.0 incompatibility
In Spring 3.1.0 there were some changes to the DefaultPersistenceUnitManager that prevented my solution from working, but this was fixed in Spring 3.1.1 so I strongly suggest to upgrade to 3.1.1 if you were using 3.1.0.
JPA really rocks!
At least until you start doing crazy stuff like me (Even if I dont really think of it as crazy. It just seemed that the JPA guys must think it's crazy since there seems to be not the smallest Support in the JPA classes for this).
The main Idea is that I have a multi-module Maven project. One containing the Service API and one or more for implementing each Service. And one Web-App Module binding all together containing all the Spring configuration for the project.
In an optimal world I wouldn't have to worry about the implementation-details of my service-modules. I would configure the datasource and entity manager in the Spring configuration of my Web-Application and in the local test Spring configurations of my service-modules, if one of them uses JPA. In this case the modue contains a META-INF/persistence.xml defining it's Entity classes and some persistence properties.
In my service modules I would use a test spring configuration to use an in-memory database for runing the unit-tests and in the Webapp I would configure a real connection to a real database (In my case MySQL).
So far so good. Unfortunately this approach doesn't work out of the box with Spring, as each entity manager has to be setup with one persistence-unit inside one of the persistence.xml files in the classpath. You will propably have configured one persistence-unit per module and when starting-up the Webapp you will get these errors:
No single default persistence unit defined in {classpath*:META-INF/persistence.xml}
An entity manager is directly coupled to one persistence unit and we don't want to configure multiple entity managers, because this would make injecting the right entity manager turn into a bean a nightmare. One soulution is, to give all the persistence-units the same name. At least this stops the entity manager from complaining. Unfortunatley it seems that we are now playing software-engineering-russian-roulette, as only one persistence.xml is used and we seem to have no influence on which one this is.
So we have to add another step. By providing a custom persistenceUnitManager we can create our own version, that reads all persistence.xml files and merges all the persistence-units. But be carefull - it only merges persistence-units who have the same name so merging:
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="de.cware.cweb.projects.ee.user"> <class>de.cware.cweb.services.user.model.JpaUserImpl</class> </persistence-unit> </persistence>
and:
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="de.cware.cweb.projects.ee.role"> <class>de.cware.cweb.services.role.model.JpaRoleImpl</class> </persistence-unit> </persistence>
would sort of result in:
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="de.cware.cweb.projects.ee.user"> <class>de.cware.cweb.services.user.model.JpaUserImpl</class> </persistence-unit> <persistence-unit name="de.cware.cweb.projects.ee.role"> <class>de.cware.cweb.services.role.model.JpaRoleImpl</class> </persistence-unit> </persistence>
Which is not what we wanted. But by naming both peristence-units "de.cware.cweb.projects.ee", the result for the entity manager is:
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="de.cware.cweb.projects.ee"> <class>de.cware.cweb.services.user.model.JpaUserImpl</class> <class>de.cware.cweb.services.role.model.JpaRoleImpl</class> </persistence-unit> </persistence>
Here comes the code for the MergingPersistenceUnitManager. I found it at the Ancientprogramming Blog. Unfortunately the blog didn't quite contain all the infos I needed to understand it and it took a little time to do so. This is why I'm reposting it here.
package de.cware.cweb.webapp.jpa; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.MutablePersistenceUnitInfo; import java.net.URL; import java.util.List; import java.util.Properties; /** * Created by IntelliJ IDEA. * User: cdutz * Date: 21.12.2009 * Time: 17:15:46 */ public class MergingPersistenceUnitManager extends DefaultPersistenceUnitManager { @Override protected void postProcessPersistenceUnitInfo(MutablePersistenceUnitInfo newPU) { super.postProcessPersistenceUnitInfo(newPU); final URL persistenceUnitRootUrl = newPU.getPersistenceUnitRootUrl(); newPU.addJarFileUrl(persistenceUnitRootUrl); final String persistenceUnitName = newPU.getPersistenceUnitName(); final MutablePersistenceUnitInfo oldPU = getPersistenceUnitInfo(persistenceUnitName); if (oldPU != null) { List<URL> urls = oldPU.getJarFileUrls(); for (URL url : urls) newPU.addJarFileUrl(url); List<String> managedClassNames = oldPU.getManagedClassNames(); for (String managedClassName : managedClassNames) newPU.addManagedClassName(managedClassName); List<String> mappingFileNames = oldPU.getMappingFileNames(); for (String mappingFileName : mappingFileNames) newPU.addMappingFileName(mappingFileName); Properties oldProperties = oldPU.getProperties(); Properties newProperties = newPU.getProperties(); newProperties.putAll(oldProperties); newPU.setProperties(newProperties); } } }
And my Spring configuration now looks like this:
<?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:webflow="http://www.springframework.org/schema/webflow-config" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/webflow-config http://www.springframework.org/schema/webflow-config/spring-webflow-config-2.0.xsd"> <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="locations"> <list> <value>classpath:jdbc.properties</value> </list> </property> </bean> <!-- Custom PersistenceUnitManager, that reads all persistence.xml files in the classpath and merges them to one single virtual persistence.xml. In order to operate properly, all persistence.xml have to define the same persistence unit name. --> <bean id="persistenceUnitManager" class="de.cware.cweb.webapp.jpa.MergingPersistenceUnitManager"> <property name="persistenceXmlLocations"> <list> <value>classpath*:META-INF/persistence.xml</value> </list> </property> <property name="defaultDataSource" ref="dataSource"/> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="jpaDialect"> <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect"/> </property> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter"> <property name="databasePlatform" value="${jpa.eclipselink.databasePlatform}"/> <property name="showSql" value="${jpa.showSql}"/> <property name="generateDdl" value="true"/> </bean> </property> <property name="dataSource" ref="dataSource"/> <property name="persistenceUnitManager" ref="persistenceUnitManager"/> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- Activates a load-time weaver for the context. Any bean within the context that implements LoadTimeWeaverAware (such as LocalContainerEntityManagerFactoryBean) will receive a reference to the autodetected load-time weaver. --> <context:load-time-weaver aspectj-weaving="on"/> </beans>
One really great goodie, I found out late in the night, was that I was able to set system wide defaults for my persistence.xml without the need to define them in each file. For example by adding this persistence.xml to my Webapp module (which itself has no JPA entites to configure):
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <!-- Dummy persistence-xml containing all the application-wide configuration parameters. Will be merged into one big persistence.xml by the MergingPersistenceUnitManager. --> <persistence-unit name="de.cware.cweb.projects.ee"> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="eclipselink.ddl-generation" value="create-tables"/> <property name="eclipselink.ddl-generation.output-mode" value="database"/> </properties> </persistence-unit>
Makes the cofuguration exclude any not specified Entites and configures eclipselink to automatically create the entity tables for me.
Using this scenario you could even switch the persistence from any JPA provider to another without having to mess with anything in the JPA-Enabled service modules.
I know this is no really new news, but I couldn't find it summed up like this anywhere using Google. Hope it helps someone
8 Comments
Anonymous
MergingPersistenceUnitManager doesn't work with Spring-orm-3.0.2.RELEASE and hibernate-3.5.1-FINAL.
This is because MutablePersistenceUnitInfo doesn't implement hibernate-jpa-2.0-api-1.0.0.Final's PersistenUnitInfo.getValidationMode().
How annoying. http://jira.springframework.org/browse/SPR-6408
We're left with no option but to manually add the <class> for the dependency jars entities.
Can't believe JPA2 has left this aside.
Christofer Dutz
Well all I can say is that I use it in the folowing configuations:
and it works like a charm ...
But I will certainly not doubt that mabe it doesn't work in all configurations.
Anonymous
Christofer, great article. It's save some time for me.
Anonymous
How about multiple connection ?
Christofer Dutz
An Entity can only be associated to one database. If however you want to have multiple entities connected to different databases, you will have to place them in different persistence contexts.
Anonymous
if i have 2 db with difference platform (MySql and Oracle), what should i do ? can you give me example configuration...
it make me crazy ...
this my persistence unit in "META-INF/persistence.xml".:
my spring-context:
can you help, what is wrong with my configuration ?
Anonymous
Hi with Spring 3.1.1 the solution give me the error:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'persistenceUnitManager' defined in class path resource [dac-mp-ee.xml]: Invocation of init method failed; nested exception is java.lang.Error: Unresolved compilation problems:
The type SpringPersistenceUnitInfo is not visible
The type SpringPersistenceUnitInfo is not visible
Type mismatch: cannot convert from element type org.springframework.orm.jpa.persistenceunit.SpringPersistenceUnitInfo to SpringPersistenceUnitInfo
The type SpringPersistenceUnitInfo is not visible
PersistenceUnitReader and SpringPersistenceUnitInfo are not visible. do you have a solution about that?
Thanks
Christofer Dutz
I am using Spring 3.1.1-RELEASE for quite some time now and am not having a single issue with it. There were problems with Spring 3.1 that were fixed in 3.1.1 so the initial solution worked again with 3.1.1.