Hibernate Reactive and Data Repositories

This section documents Hibernate ORM 7.4.x (User Guide, Introduction, Query Language Guide, Data Repositories Guide), Jakarta Persistence 3.2, Hibernate Search 8.4.x, and the Hibernate Validator / Hibernate Reactive references — which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against those official docs before being relied on in production.

Three older reference books were consulted as bibliography only while preparing these pages and are not the primary or main source for any page. All three predate Jakarta Persistence 3.2 and Hibernate ORM 6/7 (the javax.persistencejakarta.persistence namespace change, the ORM 6 query-engine rewrite, the Hibernate Search 6+ Elasticsearch backend), so the official documentation above wins on any discrepancy.

This section’s bibliography lists the reference material consulted while preparing these pages.

Everything else in this section documents blocking Hibernate ORM. This page covers Hibernate Reactive — a genuinely different, non-blocking execution model — and Hibernate Data Repositories, the Jakarta Data implementation built on blocking Hibernate ORM. The two are easy to conflate by name; they are unrelated to each other beyond sharing the Hibernate project.

Hibernate Reactive fundamentals

Hibernate Reactive offers two async APIs: the recommended, Mutiny-based Mutiny.SessionFactory/ Mutiny.Session, and a legacy CompletionStage-based Stage.SessionFactory/Stage.Session. This page uses the Mutiny API throughout, as the reference documentation itself recommends.

Two facts shape everything else on this page:

  • Hibernate Reactive requires a genuinely non-blocking Vert.x reactive SQL client per database (dedicated clients exist for PostgreSQL, MySQL, DB2, SQL Server, Oracle, and CockroachDB) instead of a JDBC driver — there is no JDBC DataSource underneath at all, so none of the connection-pool/DataSource configuration used elsewhere in this section applies.

  • Spring Data does not support Hibernate Reactive — there is no ReactiveCrudRepository-style repository abstraction for it. Every reactive interaction with Hibernate Reactive goes through the Mutiny.SessionFactory API directly.

Obtaining the Mutiny.SessionFactory: unwrap it from a JPA EntityManagerFactory built with the org.hibernate.reactive.provider.ReactivePersistenceProvider persistence provider, then expose it as a Spring @Bean:

@Configuration
public class HibernateReactiveConfig {

    @Bean
    public Mutiny.SessionFactory sessionFactory() {
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("library-reactive");
        return emf.unwrap(Mutiny.SessionFactory.class);
    }
}

Accessing the session/entity manager reactively

Mutiny.SessionFactory has no standalone "get me a session" call the way blocking Hibernate’s @PersistenceContext EntityManager em does. Instead, the session exists only for the duration of a callback’s Uni:

// read / non-transactional work
public Uni<Book> findBook(Long id) {
    return sessionFactory.withSession(session -> session.find(Book.class, id));
}

// transactional work -- see "Transaction handling" below
public Uni<Order> placeOrder(Order order) {
    return sessionFactory.withTransaction((session, tx) ->
            session.persist(order).chain(session::flush).replaceWith(order));
}

withSession(Function<Mutiny.Session, Uni<T>> work) opens (or reuses) a Mutiny.Session for the duration of work, returning the Uni<T> the callback produces. withTransaction(BiFunction<Mutiny.Session, Mutiny.Transaction, Uni<T>> work) does the same within a transaction. A nested withSession/withTransaction call made from inside an already-open one reuses the same session rather than opening a second one — calling withTransaction again inside an outer withTransaction block joins the existing transaction/session instead of nesting a new one.

How the session is bound to the current unit of work — and why this differs from Reactor

This is the detail every other page in this section’s cross-linked Reactive Programming page does not prepare you for. Per the Hibernate Reactive reference documentation, withSession()/ withTransaction() associate the reactive session with the current Vert.x (duplicated) Context — not a Reactor Context, and not a ThreadLocal. Every Hibernate Reactive call must run on a Vert.x event-loop thread; calling from any other thread fails with:

HR000068: This method should exclusively be invoked from a Vert.x EventLoop thread

Contrast this directly with Reactive transactions and thread affinity, which documents how Spring’s own ReactiveTransactionManager rides the Reactor Context instead — state that survives thread hops, scoped to one unit of work rather than one thread. The two mechanisms look similar at that level of description, but they are two entirely independent context-propagation systems that share no state whatsoever. A Reactor pipeline calling into Hibernate Reactive crosses a real context boundary at that call, even though no explicit .subscribe() is written there — the "don’t break the chain" rules on that page are about Reactor’s own Context and say nothing, by themselves, about the Vert.x Context Hibernate Reactive needs to already be present.

Transaction handling

withTransaction((session, tx) → …​) demarcates the transaction programmatically, not declaratively: it begins a transaction, runs the callback, and commits when the returned Uni completes successfully, or rolls back if it completes with a failure — including an exception thrown synchronously inside the callback. Conceptually this is the Mutiny counterpart of TransactionalOperator.transactional(…​) (Reactive transactions and thread affinity), but there is no ReactiveTransactionManager implementation for Hibernate Reactive, and Spring’s declarative @Transactional on a method returning a Publisher does not apply to it — withTransaction is the only transaction-demarcation mechanism available.

public Uni<Order> placeOrder(Order order) {
    return sessionFactory.withTransaction((session, tx) ->
            session.persist(order).chain(session::flush).replaceWith(order));
}

Bridging Uni/Multi to Mono/Flux

The io.smallrye.reactive:mutiny-reactor Maven artifact (see the SmallRye Mutiny "Using other reactive programming libraries" guide) provides UniReactorConverters/MultiReactorConverters for the Hibernate-Reactive-to-Spring direction:

public Mono<Book> findBookReactor(Long id) {
    Uni<Book> uni = sessionFactory.withSession(session -> session.find(Book.class, id));
    return uni.convert().with(UniReactorConverters.toMono());
}

public Flux<Book> allBooksReactor() {
    Multi<Book> multi = sessionFactory.withSession(session ->
            session.createQuery("from Book", Book.class).getResultList())
            .onItem().transformToMulti(list -> Multi.createFrom().iterable(list));
    return multi.convert().with(MultiReactorConverters.toFlux());
}

For the reverse direction (Reactor to Mutiny), plain Reactive-Streams interop needs no extra dependency: Uni.createFrom().publisher(mono) / Multi.createFrom().publisher(flux).

The bridge converts only the value stream — it does not carry Reactor Context into the Mutiny side or vice versa (per the context-boundary point above). Anything read from Reactor Context upstream of the conversion must be passed into the Hibernate Reactive call explicitly, as a method parameter — never assumed to cross the bridge implicitly.

Running under Spring WebFlux specifically

The Vert.x Context requirement is independent of which HTTP server WebFlux uses. Spring WebFlux’s default Reactor Netty server does not itself provide a Vert.x event loop, so an application embeds/starts its own io.vertx.core.Vertx instance and runs Hibernate Reactive calls on it explicitly (e.g. via vertx.getOrCreateContext() / Vertx.currentContext().runOnContext(…​)), rather than assuming the WebFlux request thread already is one.

Quarkus’s Hibernate Reactive guide is worth mentioning only as a contrast: Quarkus wires this automatically because it runs on Vert.x natively. This section does not use Quarkus as part of its own stack — the point above about explicitly embedding Vert.x is what a Spring WebFlux application specifically needs to do that a Quarkus application does not.

Hibernate Data Repositories

Separately from Hibernate Reactive, Hibernate ORM 7 implements the Jakarta Data @Repository model: implementations generated at compile time by HibernateProcessor, using @Find/@Query/@HQL/@SQL annotated methods, backed by StatelessSession.

@Repository
public interface BookRepository {
    @Find
    List<Book> findByAuthorName(String authorName);

    @HQL("from Book where publishedYear > :year")
    List<Book> recentBooks(int year);
}

State plainly: this is the blocking-Hibernate-ORM Jakarta Data implementation, not Hibernate Reactive — it runs on StatelessSession over a normal JDBC connection, with none of the Vert.x-Context requirements above. It differs in intent from Spring Data JPA in that Jakarta Data is a specification-level, provider-agnostic repository model (any Jakarta Data provider could implement BookRepository above), whereas Spring Data JPA’s JpaRepository is Spring-specific and Hibernate/JPA-specific by design.

Hibernate Search does not support Hibernate Reactive

Per the Hibernate team, hibernate-search-mapper-orm does not work with Hibernate Reactive, and there is no officially supported or planned reactive Hibernate Search integration — tracked as the still-open feature request HSEARCH-4922, with no committed timeline (see also the Hibernate team’s own confirmation on the Hibernate community forum).

For full-text search from a Hibernate-Reactive-based service, call Elasticsearch/OpenSearch directly with a non-blocking client instead of going through Hibernate Search’s ORM mapper — see Hibernate Search Backends (explicitly noting it is blocking-Hibernate-only) and Elasticsearch for the reactive Elasticsearch client / Spring Data Elasticsearch reactive path.

Cross-references

See also Reactive Programming, anchored specifically at Reactive transactions and thread affinity (per the context-boundary discussion above) and at Bridging to blocking code safely for the general principle it already establishes — that a hard technology boundary (there: a blocking call; here: a different reactive runtime’s own context) must be crossed deliberately and explicitly, never assumed.

Reactor chain to Vert.x-bound session and back

sequenceDiagram participant Controller as WebFlux controller (Reactor chain) participant Bridge as Uni/Mono bridge (mutiny-reactor) participant Session as Mutiny.Session (Vert.x Context) participant Client as Vert.x reactive SQL client participant DB as Database Controller->>Bridge: Uni.createFrom().publisher(mono): Reactor Context ends here Note over Bridge,Session: context boundary -- Reactor Context does NOT cross,
Vert.x Context begins here Bridge->>Session: sessionFactory.withTransaction(session, tx, work) Session->>Client: non-blocking SQL call bound to current Vert.x Context Client->>DB: query/update DB-->>Client: result Client-->>Session: Uni of the result Session-->>Bridge: Uni of the result Bridge-->>Controller: UniReactorConverters.toMono(): back into Reactor Context