Skip to main content

Bootstrapping EntityManager in Hibernate 7 (Jakarta Persistence 3.2) – XML vs Programmatic Guide

Every other Hibernate article on this blog runs inside Spring Boot’s auto-configured EntityManagerFactory. This rewrite steps outside it: real XML bootstrapping via persistence.xml, and Jakarta Persistence 3.2’s new programmatic PersistenceConfiguration builder, verified via javap against the real jar. The centerpiece is a claim checked two independent ways — does reusing a persistence-unit name pull in that unit’s XML settings? A native SELECT DATABASE() query and Hibernate’s own duplicate bootstrap log entries both confirm it doesn’t. Also corrects a stale javax.persistence.PersistenceException claim to the real jakarta.persistence.PersistenceException, and measures what an EntityManagerFactory actually costs to build.

Every other Hibernate article on this blog runs inside Spring Boot, where @Autowired EntityManagerFactory just works and you never think about where it came from. This one deliberately steps outside that safety net: how do you get a working EntityManager in a Java SE application, a batch job, or a plain unit test, with no Spring container anywhere?

There are two ways to answer that—an XML descriptor Hibernate has supported for years, and a fully programmatic builder that’s brand new in Jakarta Persistence 3.2. Both are exercised for real below, along with the one claim about the programmatic path most worth being skeptical of: does reusing an existing persistence-unit name quietly pull in that unit’s XML settings? It doesn’t, and this article proves it two independent ways rather than asserting it.

Versions used in this article. Hibernate ORM 7.4.5.Final, Jakarta Persistence 3.2.0 (where PersistenceConfiguration is new), Java 25 (Temurin LTS), H2 2.4.240. Every test below is deliberately plain JUnit, not @SpringBootTest—the whole point is bootstrapping outside a DI container.

The factory is the expensive part; the EntityManager is cheap

Before the code, the mental model worth having: EntityManagerFactory and EntityManager are not two names for the same thing at different points in a request—they have completely different lifetimes and completely different costs.

One expensive factory, many cheap workers EntityManagerFactory built ONCE, application-scoped scans entities, validates config, initializes the ServiceRegistry measured: ~1630 ms to build EntityManager (many, per unit of work) short-lived, borrowed from the factory, represents one transaction or request measured: ~36 ms to create

Those numbers aren’t illustrative round figures—they’re a real measurement, covered near the end of this article. The ratio is the whole reason “never create an EntityManagerFactory per request” is a rule: the factory call did roughly 45× the work of creating an EntityManager from an already-built factory.

Bootstrapping from XML, the way it’s worked for years

A persistence.xml on the classpath at the conventional META-INF/persistence.xml location, resolved purely by unit name—no code beyond the name string:

<persistence-unit name="XmlBootstrapPU" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>com.ankurm.hibernatedemo.bootstrap.BootstrapUser</class>
    <properties>
        <property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
        <property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:bootstrap-xml;DB_CLOSE_DELAY=-1"/>
        <property name="hibernate.hbm2ddl.auto" value="create-drop"/>
    </properties>
</persistence-unit>

Full file: persistence.xml. Used from Java by name alone:

try (EntityManagerFactory emf = Persistence.createEntityManagerFactory("XmlBootstrapPU")) {
    EntityManager em = emf.createEntityManager();
    em.getTransaction().begin();
    BootstrapUser user = new BootstrapUser("Ankur", "[email protected]");
    em.persist(user);
    em.getTransaction().commit();
}
xmlBootstrap: persisted and reloaded user id=1

Source: persistence.xml, EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt.

Bootstrapping without any XML at all: PersistenceConfiguration

Jakarta Persistence 3.2 adds a fluent, programmatic builder—confirmed via javap against the real jakarta.persistence-api-3.2.0.jar, not inferred from documentation. Its constants map to the same string property keys the XML form uses (JDBC_URL, JDBC_DRIVER, and so on); there’s no dedicated .jdbcUrl(String) method, connection details go through .property(...):

PersistenceConfiguration config = new PersistenceConfiguration("ProgrammaticPU")
    .provider("org.hibernate.jpa.HibernatePersistenceProvider")
    .managedClass(BootstrapUser.class)
    .property(PersistenceConfiguration.JDBC_DRIVER, "org.h2.Driver")
    .property(PersistenceConfiguration.JDBC_URL, "jdbc:h2:mem:bootstrap-programmatic;DB_CLOSE_DELAY=-1")
    .property(PersistenceConfiguration.JDBC_USER, "sa")
    .property(PersistenceConfiguration.JDBC_PASSWORD, "")
    .property("hibernate.hbm2ddl.auto", "create-drop");

try (EntityManagerFactory emf = config.createEntityManagerFactory()) {
    // ...
}
programmaticBootstrap: persisted user id=1 with zero persistence.xml units named 'ProgrammaticPU'

Source: EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt.

That result’s name is the point: "ProgrammaticPU" has no matching <persistence-unit> anywhere in persistence.xml. This only works at all if the builder genuinely constructs a persistence unit from code, with zero XML lookup involved—which sets up the next section’s claim.

Reusing a persistence-unit name does not search for XML—proven two ways

This is the claim in this article most worth being skeptical of, so it’s checked twice, independently, rather than taken on faith.

Setup: build a PersistenceConfiguration named "XmlBootstrapPU"—the exact same name as the real XML-defined unit above—but with entirely different properties, pointed at a third H2 database built purely in code.

Same unit name, two unrelated databases Real XML unit: “XmlBootstrapPU” jdbc:h2:mem:bootstrap-xml defined in persistence.xml Programmatic config, same name jdbc:h2:mem:bootstrap-namecollision built purely in Java code Two independent proofs the name did NOT merge them: 1. SELECT DATABASE() returns BOOTSTRAP-NAMECOLLISION, not BOOTSTRAP-XML 2. Hibernate logs “Processing PersistenceUnitInfo [name: XmlBootstrapPU]” TWICE, with two different JDBC URLs, 104ms apart in the same run

First proof—a native query asking the database itself which database it is:

List<Object> row = em.createNativeQuery("SELECT DATABASE()").getResultList();
String actualDb = (String) row.get(0);
persistenceUnitNameCollision: connected database = BOOTSTRAP-NAMECOLLISION (unit name 'XmlBootstrapPU' reused on purpose)

Source: EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt. If the reused name had triggered any XML lookup or merge, the factory would be connected to bootstrap-xml instead. It isn’t.

Second, independent proof—Hibernate’s own bootstrap log, for the same test run, showing two completely different PersistenceUnitInfo entries under the identical name at two different points:

23:40:17.710 [main] INFO org.hibernate.orm.jpa -- HHH008540: Processing PersistenceUnitInfo [name: XmlBootstrapPU]
	Database JDBC URL [jdbc:h2:mem:bootstrap-namecollision;DB_CLOSE_DELAY=-1]
	Default catalog/schema: BOOTSTRAP-NAMECOLLISION/PUBLIC

23:40:17.814 [main] INFO org.hibernate.orm.jpa -- HHH008540: Processing PersistenceUnitInfo [name: XmlBootstrapPU]
	Database JDBC URL [jdbc:h2:mem:bootstrap-xml;DB_CLOSE_DELAY=-1]
	Default catalog/schema: BOOTSTRAP-XML/PUBLIC

Same persistence-unit name, two entirely different JDBC URLs, logged 104ms apart in the same JVM. The programmatic config’s own properties won completely, both times a PersistenceConfiguration was used, regardless of what XML on the classpath also defined under that name.

Source: EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt (the full log block, both HHH008540 lines included verbatim).

Good news and a trap in the same fact. A programmatic config can safely reuse a production-sounding unit name in a test without risk of inheriting production XML settings from the classpath. But it also means a typo that happens to collide with a real unit name won’t get “caught” by any merge behavior—there is none. Treat the name purely as a label once you’re on the PersistenceConfiguration path.

An unconfigured name fails loudly—and the exception type has changed

Persistence.createEntityManagerFactory(name) with no properties map and no matching config or XML unit has nowhere left to look:

assertThatThrownBy(() -> Persistence.createEntityManagerFactory("TotallyUnknownPU"))
    .isInstanceOf(PersistenceException.class);
unconfiguredUnitName: jakarta.persistence.PersistenceException: No Persistence provider for EntityManager named TotallyUnknownPU

Source: EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt.

Correction to an earlier version of this article. It previously stated this throws javax.persistence.PersistenceException. That namespace is stale—Jakarta EE moved javax.persistence.* to jakarta.persistence.* starting with Jakarta Persistence 3.0, and Hibernate 7.4.5 / Jakarta Persistence 3.2.0 only knows the jakarta.* form. The real, verified exception is jakarta.persistence.PersistenceException, confirmed by the run above.

What a factory actually costs to build

Not a reproduction of Metaspace exhaustion—that needs sustained, uncollectable class-loader growth across thousands of factories, and isn’t something to deliberately trigger in a shared sandbox. What is safely measurable in one run: factory-creation timing against EntityManager-creation timing, from the same configuration:

repeatedFactoryCreation: createEntityManagerFactory() took 1630 ms, createEntityManager() took 36 ms -- the factory call is the one doing schema validation, service registry bootstrap, and metadata scanning; the EntityManager call is comparatively trivial

Source: EntityManagerBootstrapTest.java, output: bootstrap-persistenceconfiguration.txt. This is a shared sandbox container, so treat the absolute milliseconds as order-of-magnitude, not a precise benchmark—but the ratio, roughly 45×, is the point. It’s the concrete number behind advice that’s usually repeated without one: never build an EntityManagerFactory per request.

Should you ever call any of this yourself?

In a Spring Boot application, essentially never. Spring’s LocalContainerEntityManagerFactoryBean—what every other Hibernate article on this blog runs on—builds exactly one EntityManagerFactory at startup and hands out EntityManagers from it per request or transaction via @PersistenceContext/@Autowired EntityManagerFactory. This article’s raw bootstrapping is for the cases genuinely outside a DI container: a standalone Java SE batch job, a library that must not assume Spring is present, or a unit test that wants to prove something about bootstrapping itself without inheriting an entire Spring context—which is exactly why every test referenced above is plain JUnit, deliberately.

Further reading

No Comments yet!

Leave a Reply

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