JEHibernate 4: Hibernate 7 without the boilerplate, from Paper to Spring Boot
Version 4 turns my plugin library into a modular persistence toolkit: HikariCP, Flyway, multi-tenancy, Envers auditing, tests against real databases, and a Spring Boot starter. What’s new and what breaks on upgrade.JEHibernate started as a small wrapper so I didn’t have to write the same 200 lines of Hibernate setup in every Minecraft plugin. With version 4 it has become a library I use just as much in Spring Boot services: four modules, Hibernate 7.1, connection pooling, migrations, multi-tenancy, and auditing, published on Maven Central.
This article replaces my post from April. So much has changed since then that an addendum wouldn’t have cut it. The code is at github.com/JExcellence/JEHibernate, currently at version 4.0.2.
Four modules instead of one jar
Up to 3.x, JEHibernate was a single artifact that pulled in Paper dependencies, Spring code, and test helpers whether you needed them or not. Version 4 separates them cleanly. Only the core is required; everything else you add as needed.
The core runs on Java 17 and up. On Java 21 or newer, the async API automatically uses virtual threads; on older JVMs it falls back to a regular thread pool.
One setup, three environments
The same repositories run in a plugin, in Spring Boot, and in a plain Java program. Only the entry point differs:
In Spring Boot the starter on the classpath is enough: as soon as a DataSource exists, the auto-configuration registers a JEHibernate bean. Defining your own bean overrides it, and jehibernate.enabled=false turns it off entirely.
Entities and repositories
Entities extend one of three base classes: LongIdEntity, UuidEntity, or StringIdEntity. All of them come with createdAt, updatedAt, a @Version column for optimistic locking, and an equals/hashCode that works in a HashSet even before the first save.
Queries without JPQL
Most queries need neither SQL nor JPQL. The query builder covers filters, sorting, pagination, and fetch joins. A page returns content and total count from the same session, so the two always match.
What version 4 brings
HikariCP as the connection pool
The pool is now HikariCP, by default with a maximum of 10 and a minimum of 2 open connections. Configure it via PoolConfig or the jehibernate.pool.* properties. getPoolHealth() tells you at any time how many connections are active and how many threads are waiting. That’s the first number I look at when a server slows down under load.
Migrations instead of ddl-auto=update
Flyway now runs before Hibernate boots, by default from classpath:db/migration. Liquibase is available as an alternative. Without Flyway on the classpath, nothing happens.
Multi-tenancy
Three strategies are built in: a tenant column per row (DISCRIMINATOR), a schema per tenant (SCHEMA), or a separate database per tenant (DATABASE). With the column strategy, the library throws if a query runs without a tenant bound, so data doesn’t end up with the wrong customer by accident.
Change history with Envers
With enableAudit() and @Audited on an entity, Hibernate Envers records every change. For each revision JEHibernate stores who made it and, with multi-tenancy, the tenant. You set the user once via AuditContext.setResolver(...), for example the player UUID or the logged-in user. It requires hibernate-envers on the classpath.
Caching and concurrent changes
AbstractCachedRepository puts a Caffeine cache in front of the repository, by ID and by a key of your choice such as the player name. Concurrent misses for the same key are collapsed into a single database query, and expiry times are jittered slightly so a bulk preload doesn’t expire all at once. For concurrent changes, OptimisticLockRetry retries an operation with growing backoff:
Tests against real databases
In-memory H2 is fast but doesn’t behave like PostgreSQL. The testing module can therefore start real databases via Testcontainers: PostgreSQL, MySQL, MariaDB, and SQL Server. The library’s own CRUD suite runs against all four and against H2. Without Docker, the container tests are skipped instead of failing.
Between tests, DatabaseReset resets the database, by TRUNCATE, recreating the schema, or rolling back the transaction.
Paper: what the library doesn’t do for you
Database work never belongs on a Minecraft server’s main thread, so the *Async methods run on their own executor. Switching back to the main thread is up to you as soon as you touch the world or a player. JEHibernate deliberately doesn’t know the Bukkit scheduler, so the core stays free of Paper dependencies.
Upgrading from 3.x
- The coordinate is now de.jexcellence.hibernate:jehibernate-core instead of JEHibernate.
- ddl-auto defaults to validate. Without migrations, temporarily set update.
- connectionPool(min, max) now configures HikariCP instead of Agroal.
- Specifications.equal is now equalTo.
- Paper-specific config loading lives in its own module, jehibernate-plugin.
What’s deliberately missing
No soft delete, no Micrometer metrics, and no open-session-in-view. Auditing is Envers-only; a custom event listener and automatic purging of old revisions are deliberately deferred in the architecture decisions. If you need them, you build them on the existing pieces today.
Installation
Questions, or a project where persistence is holding you back? Get in touch, it’s exactly the kind of work I enjoy.