# 08 — Stored procedures with Hibernate 7 (merges posts 4867 + 4881) [← Previous: 07 — Immutable entities](07-immutable-entities.md) | [Next: 09 — Testing with in-memory databases →](09-testing-in-memory-databases.md) Backs [ankurm.com: stored procedures with Hibernate 7](https://ankurm.com/mastering-stored-procedures-with-hibernate-7-a-deep-dive-for-high-performance-java-apps/). Posts 4867 (`@NamedStoredProcedureQuery`) and 4881 (general stored-procedure guide) cover the same ground from two angles — annotation-driven metadata and the programmatic `StoredProcedureQuery` API — and both demo a MySQL `DELIMITER //` procedure that was never actually run. This chapter merges them into one topic and, for the first time, executes every example against a real database: **HSQLDB 2.7.3**, which supports genuine SQL/PSM `CREATE PROCEDURE` with IN/OUT/INOUT parameters and cursor-backed result sets. Verified on Hibernate ORM 7.4.5.Final, jakarta.persistence-api 3.2.0, HSQLDB 2.7.3, JDK 25. Test classes: [`StoredProcedureHappyPathTest`](../src/test/java/com/ankurm/hibernatedemo/procedure/StoredProcedureHappyPathTest.java), [`ProcedureFailureModesTest`](../src/test/java/com/ankurm/hibernatedemo/procedure/ProcedureFailureModesTest.java), [`ProcedureSchemaSupport`](../src/test/java/com/ankurm/hibernatedemo/procedure/ProcedureSchemaSupport.java) (the real `CREATE PROCEDURE` DDL). Entities: [`procedure/`](../src/main/java/com/ankurm/hibernatedemo/procedure/) — [`ProcEmployee`](../src/main/java/com/ankurm/hibernatedemo/procedure/ProcEmployee.java), [`EmployeeSummary`](../src/main/java/com/ankurm/hibernatedemo/procedure/EmployeeSummary.java). ```bash ./mvnw -Dtest=StoredProcedureHappyPathTest,ProcedureFailureModesTest test ``` Raw output: [`procedure-happy-path.txt`](output/procedure-happy-path.txt), [`procedure-failure-modes.txt`](output/procedure-failure-modes.txt), [`procedure-hsqldb-jdbc-driver-quirk.txt`](output/procedure-hsqldb-jdbc-driver-quirk.txt), [`procedure-javap-api-surface.txt`](output/procedure-javap-api-surface.txt). ## The API surface, confirmed by javap, not by reading docs `jakarta.persistence-api-3.2.0.jar` really does contain `NamedStoredProcedureQuery`, `StoredProcedureParameter`, and `StoredProcedureQuery` exactly where both articles say. All three parameter modes (`IN`, `OUT`, `INOUT`) plus `REF_CURSOR` exist on `ParameterMode`. This part of the articles was accurate; it just needed a receipt. ## IN / OUT — three call styles, same real result A single HSQLDB procedure, `GET_TAX(IN emp_id INT, OUT tax_amount DECIMAL(10,2))`, computing `salary * 0.15`, was called three ways and produced the same live database result each time ([`docs/output/procedure-happy-path.txt`](output/procedure-happy-path.txt)): - `@NamedStoredProcedureQuery` + `EntityManager.createNamedStoredProcedureQuery(name)` - unnamed, via `EntityManager.createStoredProcedureQuery("GET_TAX")` + `registerStoredProcedureParameter(...)` - unnamed, via `Session.createStoredProcedureQuery("GET_TAX")` (Hibernate-native entry point, same JPA-shaped return type) Employee id 1, salary 50,000.00 → tax **7,500.00**, exactly as the (previously unexecuted) article predicted. ## INOUT — round-trips through the same parameter slot `ADJUST_SALARY(INOUT sal DECIMAL(10,2), IN bonus_pct DECIMAL(5,2))` takes 1,000.00 and 10%, returns **1,100.00** through `getOutputParameterValue("sal")` — the same parameter object used for both the input bind and the output read. This confirms the articles' basic INOUT claim; the part they didn't cover is what happens when you get the setup wrong (see Pitfalls below). ## Result sets: where Hibernate + HSQLDB genuinely does not work This needed to be said plainly rather than faked. A `DYNAMIC RESULT SETS 1` procedure that opens a cursor (`LIST_EMPLOYEES()`) works perfectly over raw JDBC — `CallableStatement.executeQuery()` returns the rows without complaint. But Hibernate's `ProcedureCallImpl` doesn't call `executeQuery()`; it calls `execute()` and trusts its boolean return to decide whether a `ResultSetOutput` exists. A raw-JDBC probe isolates the exact defect: ``` execute() returned=false <- HSQLDB driver says "no result set" getResultSet() = <- but there is one ``` Because Hibernate believes the (wrong) `false`, both `@NamedStoredProcedureQuery(resultClasses = ProcEmployee.class)` and `createStoredProcedureQuery(name, "EmployeeSummaryMapping")` (the `@SqlResultSetMapping`-to-DTO path) fail identically: ``` java.lang.IllegalStateException: Current CallableStatement was not a ResultSet, but getResultList was called ``` **This is a real HSQLDB-JDBC-driver incompatibility, not a mapping mistake** — verified by reproducing the underlying JDBC behaviour outside Hibernate entirely ([`docs/output/procedure-hsqldb-jdbc-driver-quirk.txt`](output/procedure-hsqldb-jdbc-driver-quirk.txt)). It blocks the "map a procedure's result set to an entity" and "map it to a DTO via `@SqlResultSetMapping`" scenarios specifically on HSQLDB + Hibernate 7.4.5. A database whose driver reports `execute()` correctly for cursor results — PostgreSQL's REF_CURSOR support, or MySQL/SQL Server's direct-result-set procedures — would not hit this; a real PostgreSQL REF_CURSOR run was not attempted in this pass (treated as optional per scope) and would be the natural follow-up if this chapter needs the mapped-result-set demo running end-to-end. One incidental, useful finding from the same probe: HSQLDB does **not** enforce the classic "consume the result set before reading OUT parameters" ordering rule some drivers impose. On a procedure with both an OUT parameter and a cursor, the OUT value reads correctly whether you read it before, interleaved with, or after draining the cursor. That specific folklore pitfall is real on some databases, not on this one — worth saying explicitly rather than repeating as universal. ## The failure modes (the actual point of this chapter) All verbatim, from `ProcedureFailureModesTest`: - **Wrong parameter name, right position** — registering a parameter under a name the procedure does not have (`"employee_id"` vs. the real `"emp_id"`) **does not fail and does not silently null out**. HSQLDB's driver calls procedures with positional `{call GET_TAX(?, ?)}` syntax — the name never reaches the database. Hibernate maps `registerStoredProcedureParameter(name, ...)` to the Nth JDBC placeholder by **registration order**, and `name` is purely a client-side label for later `setParameter`/`getOutputParameterValue` calls. The call still returns the correct 7,500.00. The corollary: **parameter order is the thing that must be right; the name is cosmetic** for a driver like this one. This directly validates the one piece of caution the original article got right ("ensure the order... matches the database definition") while correcting the implicit assumption that a name mismatch would be caught. - **`ParameterMode` mismatch** — registering the real IN parameter as `OUT` throws immediately at registration/bind time: `org.hibernate.exception.GenericJDBCException: Unable to register CallableStatement OUT parameter [Invalid argument in JDBC call: Not OUT or INOUT mode for parameter: 1]`. Swapping the IN/OUT slots by position produces the identical error — this is the one failure mode that *does* fail loudly and immediately, unlike the name mismatch above. - **`getResultList()` on a procedure with no result set** — throws the exact same `IllegalStateException: Current CallableStatement was not a ResultSet, but getResultList was called` as the HSQLDB result-set incompatibility above. Same exception, two different causes (one is a real absence of a result set, the other is a driver misreporting one that exists) — worth knowing they're indistinguishable from the exception alone. - **"Forgetting" `execute()`** — corrects another assumption. Calling `getOutputParameterValue()` without an explicit `execute()` call first does **not** throw "you forgot to call execute()". Hibernate's `ProcedureCallImpl` lazily triggers the JDBC execution itself the first time an output is requested, and returns the correct value. The "forgetting execute()" pitfall from blog folklore is not reproducible against Hibernate 7.4.5's `StoredProcedureQuery` for OUT-parameter access. ## Flush behaviour and the persistence context — the important trap, confirmed Persisting a new row and calling a procedure that counts rows **in the same transaction, without an explicit flush**, does **not** see the new row: ``` COUNT_EMPLOYEES before persisting a new row = 2 COUNT_EMPLOYEES after persist() but WITHOUT an explicit flush() = 2 COUNT_EMPLOYEES after an explicit flush() = 3 ``` Stored procedure calls do not trigger Hibernate's usual auto-flush-before-query behaviour. This matters because HQL, Criteria, and even plain native queries against a synchronized entity/table normally DO auto-flush first. A second test closes the obvious escape hatch: the Hibernate-native `ProcedureCall`'s `addSynchronizedEntityClass(...)` — documented, for HQL/native queries, to force exactly this kind of auto-flush — has **no effect** when called on a `ProcedureCall`. An unflushed `persist()` stayed invisible to `COUNT_EMPLOYEES` even after declaring the synchronization. The practical rule: **always flush explicitly before calling a stored procedure that needs to see pending changes in the same transaction; there is no annotation-level escape hatch.** Cache implications (2LC / query cache) were not independently re-measured here beyond confirming the flush behaviour above — the FAQ claim that mutating procedures leave L2 cache entries stale until manually evicted is consistent with the "no auto-flush, no auto-invalidate" pattern observed and is the safe assumption to keep in the merged chapter. ## `ProcedureCall` (Hibernate-native) vs `StoredProcedureQuery` (JPA) `javap org.hibernate.procedure.ProcedureCall` ([`docs/output/procedure-javap-api-surface.txt`](output/procedure-javap-api-surface.txt)) confirms it extends `jakarta.persistence.StoredProcedureQuery` — every JPA method is available — and adds, among others: - `markAsFunctionCall(Class | int | Type)` — calling a database **function**, not just a procedure, something the JPA-standard `StoredProcedureQuery` has no direct concept of. `FUNCTION_RETURN_TYPE_HINT` backs this. `getFunctionReturn()` retrieves the typed function result separately from OUT parameters. - `addSynchronizedQuerySpace(String)` / `addSynchronizedEntityName(String)` / `addSynchronizedEntityClass(Class)` — inherited from `SynchronizeableQuery`; present on the native API but (per above) inert for auto-flush purposes on procedure calls specifically. - Typed parameter registration via `jakarta.persistence.metamodel.Type` in addition to `Class`, and `getRegisteredParameters()` / `getParameterRegistration(...)` for introspecting what's already bound. - `AutoCloseable` — `ProcedureCall` can be used in try-with-resources; `StoredProcedureQuery` cannot. For portable, Jakarta-EE-standard code, `EntityManager.createStoredProcedureQuery(...)` / `@NamedStoredProcedureQuery` is the right default — everything in the "happy path" section above works identically through it. Reach for `Session.createStoredProcedureCall(...)` specifically for function calls (`markAsFunctionCall`) or when you need to introspect parameter registrations programmatically; do not reach for it expecting `addSynchronizedEntityClass` to save you a manual `flush()`. [← Previous: 07 — Immutable entities](07-immutable-entities.md) | [Next: 09 — Testing with in-memory databases →](09-testing-in-memory-databases.md)