Skip to end of metadata
Go to start of metadata

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 (smile)

  • No labels

8 Comments

  1. 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.

    1. Well all I can say is that I use it in the folowing configuations:

      • on the JPA1 side together with spring-orm 2.5.6 and 2.5.6.SEC1 and hibernate-core 3.3.2.GA
      • on the JPA2 side together with spring-orm 2.5.6.SEC1 and eclipselink 1.2.0
        and it works like a charm ...

      But I will certainly not doubt that mabe it doesn't work in all configurations.

  2. Anonymous

    Christofer, great article. It's save some time for me.

  3. Anonymous

    How about multiple connection ?

    1. 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.

      1. 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".:

        <?xml version="1.0" encoding="UTF-8"?>
        <persistence version="1.0" 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">
            <persistence-unit name="mySqlUnit" transaction-type="RESOURCE_LOCAL">
                <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
                <class>co.id.fund.model.Investor</class>
                <exclude-unlisted-classes>false</exclude-unlisted-classes>
                <properties>
                    <property name="javax.persistence.jdbc.user" value="root"/>
                    <property name="javax.persistence.jdbc.password" value="root"/>
                    <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
                    <property name="javax.persistence.jdbc.url"
                              value="jdbc:mysql://localhost:3306/ctidb?createDatabaseIfNotExist=true&amp;amp;useUnicode=true&amp;amp;characterEncoding=utf-8&amp;amp;autoReconnect=true"/>
                </properties>
            </persistence-unit>
            <persistence-unit name="oracleUnit" transaction-type="RESOURCE_LOCAL">
                <provider>org.eclipse.persistence.jpa.PersistenceProvider</provider>
                <class>co.id.model.report.DailyTransaction</class>
                <exclude-unlisted-classes>false</exclude-unlisted-classes>
                <properties>
                    <property name="javax.persistence.jdbc.user" value="SECURPT"/>
                    <property name="javax.persistence.jdbc.password" value="securreport"/>
                    <property name="javax.persistence.jdbc.driver" value="oracle.jdbc.driver.OracleDriver"/>
                    <property name="javax.persistence.jdbc.url" value="jdbc:oracle:thin:@//10.255.20.115:1521/support"/>
                </properties>
            </persistence-unit>
        </persistence> 

         

        my spring-context:

        <?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/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
            <context:annotation-config/>
            <aop:aspectj-autoproxy/>
            <bean id="jpaDialect" class="org.springframework.orm.jpa.vendor.EclipseLinkJpaDialect"/>
            <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
                <property name="databasePlatform" value="org.eclipse.persistence.platform.database.MySQLPlatform"/>
                <property name="showSql" value="true"/>
                <property name="generateDdl" value="true"/>
            </bean>
            <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
                <property name="jpaDialect" ref="jpaDialect"/>
                <property name="persistenceUnitManager">
                    <bean class="co.id.telkomsigma.base.util.jpa.MergingPersistenceUnitManager" >
                        <property name="persistenceXmlLocations" >
                            <list>
                                <value>classpath:/META-INF/persistence.xml</value>
                            </list>
                        </property>
                    </bean>
                </property>
            </bean>
            <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
                <property name="entityManagerFactory" ref="entityManagerFactory"/>
                <property name="jpaDialect" ref="jpaDialect"/>
            </bean>
            <tx:annotation-driven transaction-manager="transactionManager"/>
            <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor"/>
        </beans>

         

        can you help, what is wrong with my configuration ?

  4. 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

     

     

     

    1. 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.