201 lines
11 KiB
Markdown
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)
|