Adding entity classes dynamically at runtime
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.
JPA doesn't offer this feature yet. Here are three options you can check out :
Programmatically loading Entity classes with JPA 2.0?
This question on SO is similar to yours. An answer reports that it feasible with Spring.JPA 2.0: Adding entity classes to PersistenceUnit *from different jar* automatically
Good pointers are given here.Last but not least, a simple work around :
1. Generate apersistence.xml
on the fly (simple XML file creation) with a new persistence unit.
2. Add persistence file to classpath dynamically (URLCLassLoader
)
3. AskPersistenceProvider
to load new persistence unit (createEntityManagerFactory
)
EDIT:
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.
EDIT:
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
- JPA 2.1 Specs : 8.2 Persistence Unit Packaging
- JPA 2.1 Specs : 8.2.1.6 mapping-file, jar-file, class, exclude-unlisted-classes
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.