advanced~2h

Stored Procedures with JPA

Some logic genuinely belongs inside the database as a stored procedure — @NamedStoredProcedureQuery and StoredProcedureQuery let you call one cleanly through JPA's parameter-binding and result-mapping conventions, without dropping to raw JDBC just for that one call.

Learning objectives

  • Call a stored procedure through JPA using both createStoredProcedureQuery and @NamedStoredProcedureQuery.
  • Register IN/OUT/INOUT parameters correctly matching a procedure's actual signature.
  • Explain why a stored procedure's logic needs the same version-control and review rigor as any other business logic.

You've now spent several chapters learning how JPA and Hibernate let you avoid writing SQL by hand — the ORM generates it for you, from your entities and your JPQL. So it might seem strange that this chapter is about calling something that's the exact opposite: SQL logic that someone deliberately wrote by hand, sitting inside the database itself, that JPA has to reach out and call rather than generate.

Here's why that matters. Not every company builds its persistence layer from scratch, in Java, using JPA from day one. Many real companies — banks, insurance firms, large e-commerce platforms — have had a database, and a team of database administrators (DBAs), for far longer than they've had whichever Java framework happens to be fashionable this decade. Those DBAs have already written, tested, and tuned real business logic as stored procedures — sometimes for years. When a new Spring Boot service needs to talk to that same database, it doesn't get to pretend those procedures don't exist. It needs a clean, correct way to call them. That's what this entire chapter is about.

📖 Story

Picture a large bank. Once a month, on the last day, something important happens: the bank "closes the books." Every account's interest gets calculated, every pending transaction gets finalized, and a mountain of numbers gets reconciled so tomorrow's balances are correct. This isn't a simple SELECT query — it's a big, careful, multi-step calculation involving millions of rows, and it has to be exactly right, every single month, because real money is on the line.

Fifteen years ago, a database administrator at this bank wrote this entire process as a stored procedure — a named, saved piece of SQL logic that lives inside the database itself, the same way a Java method lives inside a .java file. Over the years, that procedure has been tested, tuned, and trusted. Nobody wants to touch it unnecessarily, and certainly nobody wants to rewrite fifteen years of tested logic in JPQL just because the bank is now building a shiny new Spring Boot microservice.

So here's the actual problem this chapter solves: how does your brand-new Spring Boot service trigger that fifteen-year-old stored procedure, safely and cleanly, using JPA — without dropping all the way down to raw JDBC just for this one call? That's it. That's the whole problem. And JPA has a clean, standard answer to it.

Let's slow down and define the two new pieces of vocabulary this chapter needs, in plain terms, before looking at any code.

Stored procedure (this one isn't a JPA term at all — it's a database term): a piece of SQL logic, given a name, saved inside the database itself — not in your Java code, not in a JPQL string, but physically stored in the database, the same way a table or an index is stored there. Once it exists, anyone connecting to that database — a Java app, a Python script, a command-line tool — can call it by name.

StoredProcedureQuery — this is JPA's answer to "how do I call one of those from my Java code." It's an interface, and when you call entityManager.createStoredProcedureQuery("procedure_name"), JPA hands you back an object implementing this interface, ready for you to attach parameters to and execute.

@NamedStoredProcedureQuery — the same capability, but declared once, ahead of time, as an annotation on one of your entity classes — giving the call a short, reusable name you can invoke from anywhere in your codebase, instead of retyping the raw procedure name and its parameter list every time.

Keep these two straight: a stored procedure is the thing that lives in the database. StoredProcedureQuery/@NamedStoredProcedureQuery are how your Java code reaches out and calls it.

Let's actually build the bank's month-end example, piece by piece, so this stops being abstract.

First, here's what the stored procedure itself might look like

Somewhere in the bank's PostgreSQL database, this already exists — written by a DBA, long before your Spring Boot service was ever a thought:

CREATE OR REPLACE PROCEDURE calculate_monthly_interest( IN p_month INT, IN p_year INT, OUT p_accounts_processed INT ) LANGUAGE plpgsql AS $$ BEGIN UPDATE accounts SET balance = balance + (balance * interest_rate / 12) WHERE EXTRACT(MONTH FROM last_updated) = p_month AND EXTRACT(YEAR FROM last_updated) = p_year; GET DIAGNOSTICS p_accounts_processed = ROW_COUNT; END; $$;

Don't worry about mastering PL/pgSQL syntax here — just notice the shape of it. It takes in a month and a year (IN parameters — values you're handing it), does its work, and hands something back out (p_accounts_processed, an OUT parameter — a value it computed and is returning to you). This is exactly the "month-end closing" logic from our story, just written in real SQL.

Now, how do you call this from Java, using JPA?

The quickest way — good for a one-off call — is createStoredProcedureQuery, where you register each parameter's direction (IN, OUT, or INOUT) explicitly, by hand:

StoredProcedureQuery query = entityManager .createStoredProcedureQuery("calculate_monthly_interest"); query.registerStoredProcedureParameter("p_month", Integer.class, ParameterMode.IN); query.registerStoredProcedureParameter("p_year", Integer.class, ParameterMode.IN); query.registerStoredProcedureParameter("p_accounts_processed", Integer.class, ParameterMode.OUT); query.setParameter("p_month", 3); query.setParameter("p_year", 2026); query.execute(); Integer processed = (Integer) query.getOutputParameterValue("p_accounts_processed"); System.out.println("Accounts updated: " + processed);

Read that top to bottom: you get a query object, you tell it exactly what parameters exist and which direction each one flows, you set the IN values, you run it, and then you pull the OUT value back out. Nothing magical — just JPA giving you a structured, Java-friendly way to do what a raw JDBC CallableStatement would otherwise make you do more manually.

The reusable version: @NamedStoredProcedureQuery

If this same procedure is going to be called from three or four different places in your codebase, writing out all that parameter registration every single time gets repetitive and error-prone. So JPA lets you declare it once, as an annotation on an entity class:

@Entity @NamedStoredProcedureQuery( name = "Account.calculateMonthlyInterest", procedureName = "calculate_monthly_interest", parameters = { @StoredProcedureParameter(name = "p_month", mode = ParameterMode.IN, type = Integer.class), @StoredProcedureParameter(name = "p_year", mode = ParameterMode.IN, type = Integer.class), @StoredProcedureParameter(name = "p_accounts_processed", mode = ParameterMode.OUT, type = Integer.class) } ) public class Account { // ... entity fields }

Now, calling it anywhere else in your codebase is much shorter:

StoredProcedureQuery query = entityManager .createNamedStoredProcedureQuery("Account.calculateMonthlyInterest"); query.setParameter("p_month", 3); query.setParameter("p_year", 2026); query.execute();

Same procedure, same result — just declared once and reused by name, the same way a @NamedQuery works for JPQL.

What if the procedure returns rows, not just an OUT value?

Some procedures don't just return one number — they return an entire result set, like a SELECT would. For that case, JPA can map the returned rows straight into your entities or a DTO, using @SqlResultSetMapping, in much the same way a native query's result gets mapped (a tool you already saw two chapters ago). This is the one place where calling a procedure still asks you to do a little of the same explicit mapping work a native query would.

So what's actually happening underneath, when you call query.execute()?

Nothing surprising, actually — and that's the whole point. JPA takes your StoredProcedureQuery and translates it directly into a plain JDBC CallableStatement — the exact same low-level object you'd use if you were calling this procedure from raw JDBC yourself, with none of JPA involved at all. JPA isn't being clever here; it's just giving you a nicer, more structured Java API to build that CallableStatement and read its results back, instead of making you construct it by hand.

This is genuinely different from everything else you've learned about JPQL in this course. When you write JPQL, Hibernate parses it, understands it, and translates it into SQL that fits your specific database. A stored procedure call isn't like that at all — Hibernate never looks inside the procedure, never understands what it does, and never touches its logic in any way. The procedure's actual SQL logic runs entirely inside the database engine itself, completely invisible to Hibernate. JPA's job here stops at "call this name, with these parameters, and hand me back what it returns" — nothing more, nothing less.

Here are a few real shapes this pattern takes in production systems, beyond our bank example:

  • A legacy insurance company's policy-renewal logic, written as a stored procedure a decade ago by a DBA team, still gets called today from a modern Spring Boot microservice via @NamedStoredProcedureQuery — nobody wants to risk reimplementing years of tested renewal-calculation logic in JPQL, so the new service just calls the old procedure.
  • A data warehouse's nightly ETL step, doing heavy, set-based aggregation across millions of rows, is often written as a stored procedure specifically because it runs far faster inside the database than it would if the data were pulled out row-by-row into application code first. A Spring Boot scheduler simply triggers it once a night.
  • A complex, multi-step reporting query that would turn into an unreadable, sprawling JPQL statement if forced into ORM shape is sometimes kept as a stored procedure instead — and called cleanly through JPA — precisely because it reads and runs better as SQL than as an object query.

A few things worth internalizing, not just memorizing:

  • Reach for a stored procedure through JPA specifically when the logic genuinely belongs in the database — heavy, set-based processing, or an existing, well-tested procedure you have no good reason to reimplement. It's not meant to be a routine alternative to ordinary JPQL for everyday application queries.
  • If a procedure gets called from more than one place in your codebase, declare it with @NamedStoredProcedureQuery rather than retyping createStoredProcedureQuery and its full parameter list every time — you get the same reusability benefit a @NamedQuery gives you for JPQL.
  • Register each parameter's mode (IN, OUT, or INOUT) so it exactly matches the procedure's real signature. A mismatched mode is one of the most common — and most confusing — sources of a stored-procedure call quietly failing or silently returning nothing useful.
  • Remember that the procedure's SQL logic is still real business logic, even though it lives outside your Java codebase. It deserves the same seriousness — version control, code review, testing — as anything else that decides how money moves or how data changes.

⚠️ Why this keeps happening

Stored procedures live in a strange in-between place: they're not in your Java codebase, not in your normal Git history, and not something most Java-focused engineers think about day to day. That "out of sight" quality is exactly why the following mistakes are so common — the procedure quietly does its job for months, until one of these gaps actually causes a problem.

  • Registering the wrong parameter mode. Marking something IN when the procedure actually treats it as OUT (or vice versa) usually doesn't throw an obvious error — it just fails confusingly, or silently returns nothing where you expected a value.
  • Treating the procedure as "someone else's problem" that doesn't need testing. Just because it lives in the database, written in SQL instead of Java, doesn't make it any less critical than your application code — if anything, since it's touched less often, it deserves more deliberate testing, not less.
  • Reaching for a stored procedure by default, for logic that would have been perfectly fine as an ordinary JPQL query — this just splits your business logic across two different places (Java and the database) for no real reason, making the system harder to understand as a whole.
  • Getting the result-set mapping wrong for a procedure that returns rows — forgetting the @SqlResultSetMapping step and ending up with raw, unmapped data instead of clean entities or DTOs.

Stored procedures earn their performance benefit specifically when the alternative is expensive: heavy, set-based calculations (like our interest calculation touching thousands of accounts at once) run measurably faster inside the database than they would if you pulled every row out to Java, processed it there, and pushed the results back. That round trip — sending data out, computing, sending it back in — is exactly the cost a well-written stored procedure avoids, by doing all of its work in the same place the data already lives.

Calling a stored procedure through JPA's parameter binding is just as safe from SQL injection as any other correctly parameterized query — JPA handles that part for you by default. But there's a subtler risk worth knowing about: if the procedure itself builds and runs dynamic SQL internally (some do, for flexibility), that inner logic can still be vulnerable to injection — just at a layer JPA can't see or protect. If you're relying on someone else's stored procedure, it's worth asking whether its own internals were written safely, not just trusting that calling it through JPA makes everything automatically safe.

Here's a genuinely easy trap to fall into: Hibernate's own statistics and query logging (which you learned to rely on a few chapters back) show you nothing useful about what happens inside a stored procedure — because, as this chapter's Internal Working section explained, Hibernate never looks inside it at all. If a stored-procedure call feels slow, you need to look at the database's own tools instead — its slow-query log, or a database-side profiler — rather than expecting Hibernate's usual visibility to help you here.

Treat the SQL that creates a stored procedure exactly like any other piece of your schema: put its CREATE PROCEDURE script inside a Flyway or Liquibase migration, version-controlled right alongside your tables and indexes. A procedure that gets edited directly in the production database, outside of migration history, creates the exact same kind of silent environment drift this course has already warned you about for schema changes in general — nobody knows what changed, or when, unless it's captured in a migration.

  1. Write a small stored procedure of your own, with one IN parameter and one OUT parameter (something simple, like counting rows matching a condition), and call it via entityManager.createStoredProcedureQuery, registering both parameter modes correctly.
  2. Take that same procedure and declare it as a @NamedStoredProcedureQuery on an entity instead, then call it by its short name via createNamedStoredProcedureQuery.
  3. Write a stored procedure that returns a result set (not just an OUT value), and map that result back into a DTO using @SqlResultSetMapping.
  4. Add your stored procedure's creation script to a Flyway or Liquibase migration file, so it's version-controlled the same way the rest of your schema is.

✓ Quick recap

  • A stored procedure is SQL logic saved inside the database itself, callable by name — not something Hibernate generates, but something it reaches out and calls.
  • StoredProcedureQuery calls one directly, with parameters registered by hand; @NamedStoredProcedureQuery declares the same call once, reusably, by name.
  • Underneath, JPA simply builds a plain JDBC CallableStatement — the procedure's own logic stays completely invisible to Hibernate.
  • Register IN/OUT/INOUT parameter modes exactly matching the procedure's real signature, and treat the procedure's SQL with the same version-control and testing discipline as any other business logic.

Want a visual for this concept?

Generate a diagram tailored to “Stored Procedures with JPA” — the AI picks whichever visual (flowchart, comparison, sequence, etc.) best fits.

Sign in to generate a visual →

Practice quiz

Next Step

Continue to JSON Columns — PostgreSQL JSONB & MySQL JSON with Hibernate← Back to all Spring Data JPA & Hibernate Mastery chapters