How to bootstrap JPA programmatically without the persistence.xml configuration file
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
One of my Twitter followers asked me if there is any documentation for bootstrapping JPA programmatically so that we can replace the persistence.xml
configuration file with a Java configuration:
Hey @vlad_mihalcea is there a way in which persistence.xml file can be replaced with Java config class? Any documentation related to this would be really helpful.
— Shyam Baitmangalkar (@SBaitmangalkar) November 28, 2017
Previously, I wrote an article about bootstrapping Hibernate without the persistence.xml
, but that solution was based on the Hibernate-specific EntityManagerFactoryBuilderImpl
.
In this article, I’m going to give you a solution that works with any JPA provider since it’s based on standard Java Persistence API.
PersistenceUnitInfo
The JPA specification PersistenceUnitInfo
interface encapsulates everything is needed for bootstrapping an EntityManagerFactory
. Typically, this interface is implemented by the JPA provider to store the info retrieved after parsing the persistence.xml
configuration file.
Because we will no longer use a persistence.xml
configuration file, we have to implement this interface ourselves. For the purpose of this test, consider the following implementation:
public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { public static final String JPA_VERSION = "2.1"; private final String persistenceUnitName; private PersistenceUnitTransactionType transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; private final List<String> managedClassNames; private final List<String> mappingFileNames = new ArrayList<>(); private final Properties properties; private DataSource jtaDataSource; private DataSource nonJtaDataSource; public PersistenceUnitInfoImpl( String persistenceUnitName, List<String> managedClassNames, Properties properties) { this.persistenceUnitName = persistenceUnitName; this.managedClassNames = managedClassNames; this.properties = properties; } @Override public String getPersistenceUnitName() { return persistenceUnitName; } @Override public String getPersistenceProviderClassName() { return HibernatePersistenceProvider.class.getName(); } @Override public PersistenceUnitTransactionType getTransactionType() { return transactionType; } @Override public DataSource getJtaDataSource() { return jtaDataSource; } public PersistenceUnitInfoImpl setJtaDataSource( DataSource jtaDataSource) { this.jtaDataSource = jtaDataSource; this.nonJtaDataSource = null; transactionType = PersistenceUnitTransactionType.JTA; return this; } @Override public DataSource getNonJtaDataSource() { return nonJtaDataSource; } public PersistenceUnitInfoImpl setNonJtaDataSource( DataSource nonJtaDataSource) { this.nonJtaDataSource = nonJtaDataSource; this.jtaDataSource = null; transactionType = PersistenceUnitTransactionType.RESOURCE_LOCAL; return this; } @Override public List<String> getMappingFileNames() { return mappingFileNames; } @Override public List<URL> getJarFileUrls() { return Collections.emptyList(); } @Override public URL getPersistenceUnitRootUrl() { return null; } @Override public List<String> getManagedClassNames() { return managedClassNames; } @Override public boolean excludeUnlistedClasses() { return false; } @Override public SharedCacheMode getSharedCacheMode() { return SharedCacheMode.UNSPECIFIED; } @Override public ValidationMode getValidationMode() { return ValidationMode.AUTO; } public Properties getProperties() { return properties; } @Override public String getPersistenceXMLSchemaVersion() { return JPA_VERSION; } @Override public ClassLoader getClassLoader() { return Thread.currentThread().getContextClassLoader(); } @Override public void addTransformer(ClassTransformer transformer) { } @Override public ClassLoader getNewTempClassLoader() { return null; } }
JUnit base class
To make it easier to reuse the bootstrapping logic, we can define an AbstractJPAProgrammaticBootstrapTest
base class which will be extended by all our unit tests that want to bootstrap without an external persistence.xml
configuration file.
The AbstractJPAProgrammaticBootstrapTest
creates an EntityManagerFactory
upon starting a new test and closes it after executing the unit test. This way, all tests run in isolation and each test class can be self-contained as well.
private EntityManagerFactory emf; public EntityManagerFactory entityManagerFactory() { return emf; } @Before public void init() { PersistenceUnitInfo persistenceUnitInfo = persistenceUnitInfo( getClass().getSimpleName() ); Map<String, Object> configuration = new HashMap<>(); Integrator integrator = integrator(); if (integrator != null) { configuration.put( "hibernate.integrator_provider", (IntegratorProvider) () -> Collections.singletonList(integrator) ); } emf = new HibernatePersistenceProvider() .createContainerEntityManagerFactory( persistenceUnitInfo, configuration ); } @After public void destroy() { emf.close(); }
The JPA standard defines the PersistenceProvider
interface defines the contract for instantiating a new EntityManagerFactory
. We are going to use the HibernatePersistenceProvider
which is a Hibernate-specific implementation of this interface. If you want to use a different JPA Provider, you have to check the Provider API for the PersistenceProvider
implementation and use that instead.
Now, let’s see what the persistenceUnitInfo
looks like:
protected PersistenceUnitInfoImpl persistenceUnitInfo(String name) { PersistenceUnitInfoImpl persistenceUnitInfo = new PersistenceUnitInfoImpl( name, entityClassNames(), properties() ); String[] resources = resources(); if (resources != null) { persistenceUnitInfo.getMappingFileNames().addAll( Arrays.asList(resources) ); } return persistenceUnitInfo; }
The entity classes and the associated XML configuration are defined by the following methods:
protected abstract Class<?>[] entities(); protected String[] resources() { return null; } protected List<String> entityClassNames() { return Arrays.asList(entities()) .stream() .map(Class::getName) .collect(Collectors.toList()); }
So, we can simply implement the entities
or extend the resources
method to provide the JPA mapping information programmatically.
The properties
method defines some basic properties needed by all tests like auto-generating the schema or providing a JDBC DataSource
to connect to the underlying database.
protected Properties properties() { Properties properties = new Properties(); properties.put( "hibernate.dialect", dataSourceProvider().hibernateDialect() ); properties.put( "hibernate.hbm2ddl.auto", "create-drop" ); DataSource dataSource = newDataSource(); if (dataSource != null) { properties.put( "hibernate.connection.datasource", dataSource ); } properties.put( "hibernate.generate_statistics", Boolean.TRUE.toString() ); return properties; }
Of course, we can extend the properties
base class method to provide additional properties.
The newDataSource
method is defined as follows:
protected DataSource newDataSource() { return proxyDataSource() ? dataSourceProxyType().dataSource( dataSourceProvider().dataSource()) : dataSourceProvider().dataSource(); } protected DataSourceProxyType dataSourceProxyType() { return DataSourceProxyType.DATA_SOURCE_PROXY; } protected boolean proxyDataSource() { return true; } protected DataSourceProvider dataSourceProvider() { return database().dataSourceProvider(); } protected Database database() { return Database.HSQLDB; }
The dataSourceProxyType
allows us to proxy the underlying JDBC DataSource
so that we can use the datasource-proxy Open-Source project to log SQL statements along with their bind parameter values.
However, you are not limited to standard JPA bootstrapping only since Hibernate allows you to integrate your own bootstrapping logic via the Integrator
mechanism.
By default, we don’t provide any Integrator
:
protected Integrator integrator() { return null; }
But if we provide an Integrator
, this one is passed to Hibernate via the hibernate.integrator_provider
configuration property.
For more details about how the Integrator
mechanism works, check out this article about accessing the database table metadata using Hibernate.
And that’s pretty much it!
Specific JUnit test
Now that we have the AbstractJPAProgrammaticBootstrapTest
in place, a specific test will look as follows:
public class BootstrapTest extends AbstractJPAProgrammaticBootstrapTest { @Override protected Class<?>[] entities() { return new Class[] { Post.class }; } @Test public void test() { doInJPA(entityManager -> { for (long id = 1; id <= 3; id++) { Post post = new Post(); post.setId(id); post.setTitle( String.format( "High-Performance Java Persistence, Part %d", id ) ); entityManager.persist(post); } }); } @Entity(name = "Post") @Table(name = "post") public static class Post { @Id private Long id; private String title; //Getters and setters omitted for brevity } }
We just have to extend the AbstractJPAProgrammaticBootstrapTest
base class and define the entities we want to use. Notice that the entities are associated with this test only so that we make sure that entity mapping changes don’t ripple to other tests.
The doInJPA
is a utility method that you can see in this article I wrote before.
If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
As you can see, bootstrapping JPA without the persistence.xml
is very easy since the Java Persistence standard defines the contract to do so.
