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

201 lines
11 KiB
Markdown

# 17 — Bootstrapping EntityManager: XML, PersistenceConfiguration, and what a factory costs
[← Previous: 16 — Criteria API](16-criteria-queries.md) | [Back to README →](../README.md)
Backs ankurm.com post 4855 (bootstrapping EntityManager).
Everything below comes from [`EntityManagerBootstrapTest`](../src/test/java/com/ankurm/hibernatedemo/bootstrap/EntityManagerBootstrapTest.java) --
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`](../src/test/resources/META-INF/persistence.xml) on the classpath at
the conventional `META-INF/persistence.xml` location, resolved purely by unit name:
```java
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, ...)`):
```java
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`](../src/test/java/com/ankurm/hibernatedemo/bootstrap/EntityManagerBootstrapTest.java),
[`persistence.xml`](../src/test/resources/META-INF/persistence.xml). Raw output:
[`docs/output/bootstrap-persistenceconfiguration.txt`](output/bootstrap-persistenceconfiguration.txt).
Going deeper:
- [`jakarta.persistence.PersistenceConfiguration` javadoc](https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/persistenceconfiguration) (`rel="nofollow"`)
## 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:
```java
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:
```java
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`](../src/test/java/com/ankurm/hibernatedemo/bootstrap/EntityManagerBootstrapTest.java).
Raw output: [`docs/output/bootstrap-persistenceconfiguration.txt`](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:
```java
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`](../src/test/java/com/ankurm/hibernatedemo/bootstrap/EntityManagerBootstrapTest.java).
Raw output: [`docs/output/bootstrap-persistenceconfiguration.txt`](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`](../src/test/java/com/ankurm/hibernatedemo/bootstrap/EntityManagerBootstrapTest.java).
Raw output: [`docs/output/bootstrap-persistenceconfiguration.txt`](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 `EntityManager`s 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:
- [`jakarta.persistence.Persistence` javadoc](https://jakarta.ee/specifications/persistence/3.2/apidocs/jakarta.persistence/jakarta/persistence/persistence) (`rel="nofollow"`)
- Spring's own [`LocalContainerEntityManagerFactoryBean`](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.html) (`rel="nofollow"`) for how the rest of this repo actually gets its `EntityManagerFactory`
## 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](16-criteria-queries.md) | [Back to README →](../README.md)