Files
hibernate-demo/docs/17-entitymanager-bootstrap.md

11 KiB

17 — Bootstrapping EntityManager: XML, PersistenceConfiguration, and what a factory costs

← Previous: 16 — Criteria API | Back to README →

Backs ankurm.com post 4855 (bootstrapping EntityManager).

Everything below comes from EntityManagerBootstrapTest -- deliberately plain JUnit, not @SpringBootTest. The whole point of this chapter is bootstrapping a raw JPA EntityManagerFactory with no Spring involved, the way a Java SE application, a batch job, or a unit test outside a Spring context would. Every other chapter in this repo runs inside Spring Boot's auto-configured persistence; this one deliberately doesn't.

Two ways to bootstrap, both exercised for real

XML: a persistence.xml on the classpath at the conventional META-INF/persistence.xml location, resolved purely by unit name:

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

Programmatic, via Jakarta Persistence 3.2's PersistenceConfiguration -- new in this spec version, confirmed present via javap against jakarta.persistence-api-3.2.0.jar (constants like JDBC_URL, JDBC_DRIVER, JDBC_USER, JDBC_PASSWORD map to the same string property keys as the XML form's <property> elements -- there's no dedicated .jdbcUrl(String) builder method, connection details go through .property(PersistenceConfiguration.JDBC_URL, ...)):

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'

That second result is the point of the test's name: "ProgrammaticPU" has no matching <persistence-unit> anywhere in persistence.xml. This only works at all if PersistenceConfiguration genuinely builds a persistence unit from code, with zero XML lookup.

Source: EntityManagerBootstrapTest, persistence.xml. Raw output: docs/output/bootstrap-persistenceconfiguration.txt.

Going deeper:

Reusing a persistence-unit name does not trigger an XML lookup — proven two ways

This is the claim most worth being skeptical of, so it's verified twice, independently.

The setup: build a PersistenceConfiguration with the name "XmlBootstrapPU" -- the exact same name as the real XML-defined unit -- but with completely different properties, pointed at a third H2 database (bootstrap-namecollision) built purely in code:

PersistenceConfiguration config = new PersistenceConfiguration("XmlBootstrapPU")
        .provider("org.hibernate.jpa.HibernatePersistenceProvider")
        .managedClass(BootstrapUser.class)
        .property(PersistenceConfiguration.JDBC_URL, "jdbc:h2:mem:bootstrap-namecollision;DB_CLOSE_DELAY=-1")
        // ...

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)

If the reused name had triggered any XML lookup or merge, the factory would be connected to bootstrap-xml (the real XML unit's database) instead. It isn't.

Second proof -- Hibernate's own bootstrap log, for the same test run, showing two completely different PersistenceUnitInfo entries logged under the identical name at two different points in the run: once for the programmatic config above, and once later when xmlBootstrap_createsFactoryFromPersistenceXmlAndPersistsAUser runs and actually does load the real XML unit:

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 happened to define under that name.

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

Trap: this is good news for testing (a programmatic config can safely reuse a production-sounding unit name without risk of accidentally inheriting production XML settings from the classpath), but it also means a typo that happens to collide with a real unit name will not 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 PersistenceConfiguration 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

Correction to the original version of this article: it claimed this throws javax.persistence.PersistenceException. That namespace is stale -- Jakarta EE moved the entire javax.persistence.* package to jakarta.persistence.* starting with Jakarta Persistence 3.0, and Hibernate 7.4.5 / Jakarta Persistence 3.2.0 (what this repo runs) only knows the jakarta.* form. The real, verified exception is jakarta.persistence.PersistenceException. (Post 4859, in the spring-boot-demo companion repo, had the identical javax → jakarta staleness in an unrelated exception type -- this class of correction has now shown up twice across this blog's Hibernate/JPA coverage, which suggests it's worth grepping your own older posts for javax.persistence if you haven't already.)

Source: EntityManagerBootstrapTest. Raw output: docs/output/bootstrap-persistenceconfiguration.txt.

Creating an EntityManagerFactory is measurably not cheap

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

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

Order of magnitude, not a precise benchmark (this ran in a shared sandbox container, and the absolute milliseconds will vary by machine) -- but the ratio is the point: the factory build did roughly 45x the work of creating an EntityManager from an already-built factory. This is the concrete, measured version of "never create an EntityManagerFactory per request" -- the advice usually gets repeated without a number attached to it.

Source: EntityManagerBootstrapTest. Raw output: docs/output/bootstrap-persistenceconfiguration.txt.

Should you ever call this yourself? In a Spring Boot application, essentially never -- Spring's LocalContainerEntityManagerFactoryBean (what every other chapter in this repo runs on) builds exactly one EntityManagerFactory at startup and hands out EntityManagers from it per request/transaction via @PersistenceContext/@Autowired EntityManagerFactory. This chapter'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 -- as here -- a unit test that wants to prove something about bootstrapping itself without inheriting a whole Spring context.

Going deeper:

Summary

Claim Verified value
XML bootstrap via persistence.xml Works, resolved purely by unit name
PersistenceConfiguration (JPA 3.2) Works with zero matching persistence.xml unit
No dedicated .jdbcUrl() builder method Confirmed via javap; JDBC settings go through .property(PersistenceConfiguration.JDBC_URL, ...)
Reusing a persistence.xml unit's name in a programmatic config Does NOT trigger any XML lookup or merge -- proven via SELECT DATABASE() and via duplicate HHH008540 log lines with different JDBC URLs
Unconfigured unit name Throws jakarta.persistence.PersistenceException (corrected from a stale javax.persistence.PersistenceException claim)
createEntityManagerFactory() vs createEntityManager() cost Factory creation measured at roughly 45x the cost of creating an EntityManager from an already-built factory

← Previous: 16 — Criteria API | Back to README →