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:

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.

Seize the deal! 40% discount. Seize the deal! 40% discount. Seize the deal! 40% discount.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.