37

I have this requirement to add entity classes to the persistence unit at runtime rather than specifying all of them in the persistence.xml. Can someone help me with the same?

I am aware that Hibernate has its own mechanism of doing the same using:

AnnotationConfiguration.addAnnotatedClass(Class), etc - You can also add hibernate config (*.hbm.xml) files programmatically.

The requirement is that without restarting the app server, I should be able to keep adding entity classes / their config (mapping) files to the persistence unit dynamically.

But the solution to programmatically add entity classes / configuration files at runtime to the persistence unit should not be specific to a JPA implementation.

user1090154
  • 371
  • 1
  • 3
  • 3

2 Answers2

33

JPA doesn't offer this feature yet. Here are three options you can check out :

  • This work around :
    1. Generate a persistence.xml on the fly (simple XML file creation) with a new persistence unit.
    2. Add persistence file to classpath dynamically (URLCLassLoader)
    3. Ask PersistenceProvider to load new persistence unit (createEntityManagerFactory)

I did implement this workaround. However, it didn't scale well as the number of entities grows (100+). I finally switched to JDBC. Here is a capture of the memory consumption (-75%) before and after the switch from JPA to JDBC.

Memory consumption reduction by switching from JPA to JDBC

Nota

If the JPA provider is Hibernate, since Hibernate 4.0, it's possible to pass directly entities to this JPA provider without declaring them in the persistence.xml file. Hibernate will handle the entities on the fly.

Here is a sample configuration of JPA 2.1 + Hibernate 4.3.7.Final without declaring any entities :

META-INF/persistence.xml

<?xml version="1.0" encoding="UTF-8" ?>
<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
    version="2.1">

    <persistence-unit name="my-persistence-unit"
        transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <properties>
            <!-- Database Properties -->
            <property name="javax.persistence.jdbc.url"
                value="jdbc:postgresql://localhost:5432/my-database" />
            <property name="javax.persistence.jdbc.user" value="login" />
            <property name="javax.persistence.jdbc.password" value="password" />

            <!-- Hibernate Properties -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
            <property name="hibernate.default_schema" value="public" />
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.show_sql" value="false" />
            <property name="hibernate.format_sql" value="true" />

            <!-- Connection Pool -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="500" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="2000" />
        </properties>
    </persistence-unit>

</persistence>

References

Stephan
  • 41,764
  • 65
  • 238
  • 329
  • 3
    Could you please give me information how to configure Hibernate to accept entities without declaring them? It seems that this is not default behaviour. – Rafal Dec 28 '14 at 18:29
  • Sorry for downvoting, after trying on other Tomcat it worked like a charm. It is default behaviour and I think was caused because of the gradle-tomcat plugin >. – lucasvc Sep 26 '15 at 11:34
7

I am late to the party, but I think this will save some people some headache. I implemented classpath scanning for pure JPA (no spring etc needed) that integrates with e.g. guice-persist if needed as well.

Here's what you need to do.

First, change the persistence.xml and add your own implementation, like:

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
         http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
         version="2.1">

<persistence-unit name="my.persistence.unit" transaction-type="RESOURCE_LOCAL">

    <provider>my.custom.package.HibernateDynamicPersistenceProvider</provider>

    <exclude-unlisted-classes>true</exclude-unlisted-classes>

    <properties>
        <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect" />
        <property name="hibernate.max_fetch_depth" value="30" />
        <property name="hibernate.hbm2ddl.auto" value="update" />
        <property name="hibernate.show_sql" value="true" />
    </properties>
</persistence-unit>

In order for the Providers to be recognised, you will have to make it discoverable. JPA discovers using the service loading mechanism, so we add:

/src/main/resources/META-INF/services/javax.persistence.spi.PersistenceProvider

This file has exactly one line:

my.custom.package.HibernateDynamicPersistenceProvider

Finally add your own provider and base it on the HibernateProvider (I base it on that since I want to use hibernate):

public class HibernateDynamicPersistenceProvider extends HibernatePersistenceProvider implements PersistenceProvider {

    private static final Logger log = Logger.getLogger(HibernateDynamicPersistenceProvider.class);

    public static final String CUSTOM_CLASSES = "CUSTOM_CLASSES";

    @Override
    protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder(
            PersistenceUnitDescriptor persistenceUnitDescriptor, Map integration, ClassLoader providedClassLoader) {

        if(persistenceUnitDescriptor instanceof ParsedPersistenceXmlDescriptor) {
            ParsedPersistenceXmlDescriptor tmp = (ParsedPersistenceXmlDescriptor) persistenceUnitDescriptor;
            Object object = integration.get("CUSTOM_CLASSES");
        }

        return super.getEntityManagerFactoryBuilder(persistenceUnitDescriptor, integration, providedClassLoader);
    }

    protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties, ClassLoader providedClassLoader) {
        log.debug( String.format("Attempting to obtain correct EntityManagerFactoryBuilder for persistenceUnitName : %s", persistenceUnitName ));

        final Map integration = wrap( properties );
        final List<ParsedPersistenceXmlDescriptor> units;
        try {
            units = PersistenceXmlParser.locatePersistenceUnits( integration );
        }
        catch (Exception e) {
            log.debug( "Unable to locate persistence units", e );
            throw new PersistenceException( "Unable to locate persistence units", e );
        }

        log.debug( String.format("Located and parsed %s persistence units; checking each", units.size() ));

        if ( persistenceUnitName == null && units.size() > 1 ) {
            // no persistence-unit name to look for was given and we found multiple persistence-units
            throw new PersistenceException( "No name provided and multiple persistence units found" );
        }

        for ( ParsedPersistenceXmlDescriptor persistenceUnit : units ) {
            log.debug( String.format(
                    "Checking persistence-unit [name=%s, explicit-provider=%s] against incoming persistence unit name [%s]",
                    persistenceUnit.getName(),
                    persistenceUnit.getProviderClassName(),
                    persistenceUnitName
            ));

            final boolean matches = persistenceUnitName == null || persistenceUnit.getName().equals( persistenceUnitName );
            if ( !matches ) {
                log.debug( "Excluding from consideration due to name mis-match" );
                continue;
            }

            // See if we (Hibernate) are the persistence provider

            String extractRequestedProviderName = ProviderChecker.extractRequestedProviderName(persistenceUnit, integration);

            if ( ! ProviderChecker.isProvider( persistenceUnit, properties ) && !(this.getClass().getName().equals(extractRequestedProviderName))) {
                log.debug( "Excluding from consideration due to provider mis-match" );
                continue;
            }

            return getEntityManagerFactoryBuilder( persistenceUnit, integration, providedClassLoader );
        }

        log.debug( "Found no matching persistence units" );
        return null;
    }
}

I had to overwrite 2 methods, first:

protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilder(
            PersistenceUnitDescriptor persistenceUnitDescriptor, Map integration, ClassLoader providedClassLoader)

This is the intercepting method. I added a custom property "CUSTOM_CLASSES" which should really be called "CUSTOM_PACKAGES" which will list all packages that need to be scanned. At this point I am a bit lazy and I will skip the actual classpath scanning, but you can do it yourself - it's quite straight forward. You can then call

tmp.addClasses("class1", "class2");

Where the classes are the ones you discovered.

The second method we are overriding is:

protected EntityManagerFactoryBuilder getEntityManagerFactoryBuilderOrNull(String persistenceUnitName, Map properties, ClassLoader providedClassLoader)

This is because the provider we are extending is hardcoded to only allow hibernate classes to create an EMF. Since we have a custom class intercepting the construction, our names don't add up. So I added:

String extractRequestedProviderName = ProviderChecker.extractRequestedProviderName(persistenceUnit, integration);

if ( ! ProviderChecker.isProvider( persistenceUnit, properties ) && !(this.getClass().getName().equals(extractRequestedProviderName))) {
        log.debug( "Excluding from consideration due to provider mis-match" );
        continue;
}

This extends the normal hibernate check to also include my custom provider to be valid.

Wola, we are done, you now have hibernate enabled classpath scanning with JPA.

Stephan
  • 41,764
  • 65
  • 238
  • 329
pandaadb
  • 6,306
  • 2
  • 22
  • 41
  • Some questions: Why did you declare CUSTOM_CLASSES constant and never use it? Which version of Hibernate are you using? – Stephan Jun 23 '16 at 15:38
  • CUSTOM_CLASSES is never used because it is only an examplory config. I don't acutally use it. I use guice to inject my provider with the actual entities I want instead. I use Hibernate 5.1.0.Final You can use the CUSTOM_CLASSES to e.g. pass in package names and then implement your own version of package discovery – pandaadb Jun 23 '16 at 15:48
  • Since Hibernate 4.0 it is possible to pass entities to it without declaring them. Did this feature disappear in 5.1.0.Final? – Stephan Jun 23 '16 at 16:37
  • 1
    I read that as well and it might still be there, however would that also create the table for that entity on the fly? My problem was that I had X libraries contributing entities, and these could change so i could not hardcode them. I could not blindly scan ALL packages, because I don't want all of those entities. I could also not just use them later (i think you are refering to them) because I don't know which ones are available and I must create all tables and relationships fresh on startup (which i believe only happens when creating the EMF), so I needed a way to hook into the creation. – pandaadb Jun 23 '16 at 16:46
  • How do you know which tables to create at startup? – Stephan Jun 23 '16 at 19:24
  • My persistence strategy is "update" (hmb2dll), which automatically creates the tables and the relationships after analysing all beans that I intend to use. That is why I need the classes to be injected before the EMF is built. This is not out of the box doable with anything (but spring as you pointed out). – pandaadb Jun 24 '16 at 08:42