Hibernate Stored Procedures: Call with StoredProcedureQuery

Call a stored procedure from Hibernate with StoredProcedureQuery: IN and OUT parameters, result sets mapped to entities, named procedures and Spring Data @Procedure.

Flow: Java code builds a StoredProcedureQuery, Hibernate sends the JDBC statement {call count_courses_by_level(?,?)}, MySQL runs the procedure and returns OUT parameter values and result set rows

A stored procedure is a named block of SQL that is stored in the database and runs with one call, for example count_courses_by_level, which counts the courses of a level. Hibernate calls stored procedures through the Jakarta Persistence StoredProcedureQuery API, in which we name the procedure and register its parameters. After the call, we read the OUT parameters or the returned rows.

We use stored procedures when the logic already exists in the database, for example a discount rule that the database team maintains and several apps call. For each call, Hibernate sends one JDBC {call …} statement.

The following example creates a MySQL procedure with an IN and an OUT parameter and calls it from Hibernate.

CREATE PROCEDURE count_courses_by_level(
    IN  p_level VARCHAR(20),
    OUT p_total INT)
BEGIN
  SELECT COUNT(*) INTO p_total FROM course WHERE level = p_level;
END
StoredProcedureQuery query = em.createStoredProcedureQuery("count_courses_by_level")
    .registerStoredProcedureParameter("p_level", String.class, ParameterMode.IN)
    .registerStoredProcedureParameter("p_total", Integer.class, ParameterMode.OUT)
    .setParameter("p_level", "beginner");

query.execute();                                              // {call count_courses_by_level(?,?)}
Integer total = (Integer) query.getOutputParameterValue("p_total");   // total = 2

Notice that we register both parameters in the order the procedure declares them, and read the OUT value by name after execute().

Next, we read result sets and call a procedure that changes data. After that, we declare the call once with @NamedStoredProcedureQuery or a Spring Data JPA @Procedure method.

1. How Does Hibernate Call a Stored Procedure?

The StoredProcedureQuery object describes the call, namely the procedure name and each parameter with its type and mode. When we run it, Hibernate turns it into a JDBC CallableStatement and MySQL runs the procedure. The results come back as OUT parameter values, as rows, or both.

Flow: Java code builds a StoredProcedureQuery, Hibernate sends the JDBC statement {call count_courses_by_level(?,?)}, MySQL runs the procedure and returns OUT parameter values and result set rows
One execute() sends one {call …} statement. OUT values and result rows come back through the same statement.

Each parameter has one of four modes, and the mode decides the direction of the value.

ModeDirectionExample
INJava to databasep_level = “beginner”
OUTDatabase to Javap_total = 2
INOUTBoth waysA value the procedure reads and changes
REF_CURSORDatabase to Java, as rowsOracle and PostgreSQL cursors

There are four ways to make the call, and all of them end in the same {call …} statement.

APIWhen to use it
em.createStoredProcedureQuery()The standard way; build the call in code
@NamedStoredProcedureQueryDeclare the call once on an entity and reuse it by name
Spring Data JPA @ProcedureCall the procedure from a repository method
Hibernate ProcedureCallHibernate’s own API; same features, not portable

2. Stored Procedure Example

The following example is an online course catalog in MySQL 9 with Hibernate 7.4 and Java 25, with one table and three procedures.

2.1. Database Setup

The course table holds three courses, and each procedure shows a different kind of output.

The course table with three rows and the three procedures: count_courses_by_level takes p_level and returns p_total, find_courses_by_level takes p_level and returns rows, apply_discount takes p_course_id and p_percent, updates the price and returns p_new_price
One procedure returns an OUT value, one returns rows, and one changes data and returns the new value.
@Entity
@Table(name = "course")
public class Course {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;
  private String level;
  private BigDecimal price;
}
CREATE PROCEDURE find_courses_by_level(IN p_level VARCHAR(20))
BEGIN
  SELECT id, title, level, price FROM course WHERE level = p_level ORDER BY title;
END

CREATE PROCEDURE apply_discount(
    IN  p_course_id BIGINT,
    IN  p_percent   INT,
    OUT p_new_price DECIMAL(8,2))
BEGIN
  UPDATE course SET price = ROUND(price * (100 - p_percent) / 100, 2) WHERE id = p_course_id;
  SELECT price INTO p_new_price FROM course WHERE id = p_course_id;
END

2.2. Reading an OUT Parameter

We register every parameter, set the IN values, call execute() and read the OUT value by name.

StoredProcedureQuery query = em.createStoredProcedureQuery("count_courses_by_level")
    .registerStoredProcedureParameter("p_level", String.class, ParameterMode.IN)
    .registerStoredProcedureParameter("p_total", Integer.class, ParameterMode.OUT)
    .setParameter("p_level", "beginner");

query.execute();
Integer total = (Integer) query.getOutputParameterValue("p_total");   // total = 2

Positions work too, starting at 1.

StoredProcedureQuery query = em.createStoredProcedureQuery("count_courses_by_level")
    .registerStoredProcedureParameter(1, String.class, ParameterMode.IN)
    .registerStoredProcedureParameter(2, Integer.class, ParameterMode.OUT)
    .setParameter(1, "beginner");

query.execute();
Integer total = (Integer) query.getOutputParameterValue(2);   // total = 2

2.3. Mapping the Result Set to Entities

When the procedure returns rows, we pass the entity class and call getResultList(), and the column names must match the entity’s columns.

List<Course> courses = em.createStoredProcedureQuery("find_courses_by_level", Course.class)
    .registerStoredProcedureParameter("p_level", String.class, ParameterMode.IN)
    .setParameter("p_level", "beginner")
    .getResultList();
// [Java Basics (beginner, 40.00), SQL Basics (beginner, 30.00)]

Without the class, each row is an Object[] with one element per column.

List<Object[]> rows = em.createStoredProcedureQuery("find_courses_by_level")
    .registerStoredProcedureParameter("p_level", String.class, ParameterMode.IN)
    .setParameter("p_level", "advanced")
    .getResultList();
// rows.get(0) = [3, Hibernate Internals, advanced, 80.00]

2.4. Calling a Procedure That Changes Data

apply\_discount updates the price and returns the new one. We call a procedure that changes data inside a transaction, so its update commits or rolls back together with the rest of the unit of work.

emf.runInTransaction(em -> {
  StoredProcedureQuery query = em.createStoredProcedureQuery("apply_discount")
      .registerStoredProcedureParameter("p_course_id", Long.class, ParameterMode.IN)
      .registerStoredProcedureParameter("p_percent", Integer.class, ParameterMode.IN)
      .registerStoredProcedureParameter("p_new_price", BigDecimal.class, ParameterMode.OUT)
      .setParameter("p_course_id", javaId)
      .setParameter("p_percent", 25);
  query.execute();
  BigDecimal price = (BigDecimal) query.getOutputParameterValue("p_new_price");   // 30.00
});

2.5. Declaring the Call With @NamedStoredProcedureQuery

A named stored procedure query declares the procedure, its parameters and its result class once, on the entity.

@NamedStoredProcedureQuery(
    name = "Course.findByLevel",
    procedureName = "find_courses_by_level",
    resultClasses = Course.class,
    parameters = @StoredProcedureParameter(name = "p_level", type = String.class, mode = ParameterMode.IN))
List<Course> advanced = em.createNamedStoredProcedureQuery("Course.findByLevel")
    .setParameter("p_level", "advanced")
    .getResultList();                                        // [Hibernate Internals (advanced, 80.00)]

The @NamedStoredProcedureQuery annotation also takes resultSetMappings and hints.

2.6. Calling It From a Spring Data JPA Repository

In a Spring application, a repository method with @Procedure makes the same call.

public interface CourseRepository extends JpaRepository<Course, Long> {

  @Procedure(procedureName = "count_courses_by_level", outputParameterName = "p_total")
  Integer countByLevel(@Param("p_level") String level);
}
Integer total = courseRepository.countByLevel("beginner");   // 2

The complete project on GitHub creates the procedures in a MySQL container with Testcontainers, runs each call and checks the results with 10 JUnit tests. It needs Docker (mvn -q compile exec:java, mvn test).

3. Stored Procedure FAQs

3.1. Does the Parameter Order Matter?

Yes. Register the parameters in the same order as the procedure declares them. Hibernate sends them by position ({call count_courses_by_level(?,?)}), even when we register them by name. Registering p_total before p_level fails.

org.hibernate.exception.GenericJDBCException: Unable to register CallableStatement OUT parameter [Parameter number 1 is not an OUT parameter] [n/a]

3.2. Why Does My Entity Still Show the Old Value After the Procedure?

The procedure changes the row in the database, so an entity that was already loaded in the same persistence context keeps its old value, and em.find() returns that cached object. We call em.refresh() to reload it.

Persistence context holds Course with price 40.00 while the database row has 30.00 after apply_discount; em.find returns the cached 40.00, em.refresh reloads 30.00
The procedure updates the row, not the object Hibernate already holds. refresh() reads the row again.
Course java = em.find(Course.class, javaId);                   // price = 40.00
// call apply_discount as in section 2.4                       // database: 30.00
BigDecimal cached = em.find(Course.class, javaId).getPrice();   // still 40.00 (cached)
em.refresh(java);
BigDecimal reloaded = java.getPrice();                         // 30.00

The first level cache returns the same object for every find() of that id in one persistence context.

3.3. How Do I Pass a Null Parameter?

We set it to null in the call from section 2.2, and Hibernate 7 sends NULL to the procedure.

query.setParameter("p_level", null);
query.execute();
Integer total = (Integer) query.getOutputParameterValue("p_total");   // 0, because level = NULL matches no row

The old hibernate.proc.param_null_passing setting from Hibernate 5 no longer exists.

3.4. Can I Use Hibernate’s ProcedureCall Instead?

Yes. ProcedureCall is Hibernate’s own API with the same features. We get it from the Session.

ProcedureCall call = em.unwrap(Session.class).createStoredProcedureCall("count_courses_by_level");
call.registerParameter("p_level", String.class, ParameterMode.IN);
call.registerParameter("p_total", Integer.class, ParameterMode.OUT);
call.setParameter("p_level", "advanced");
Integer total = (Integer) call.getOutputs().getOutputParameterValue("p_total");   // 1

We prefer StoredProcedureQuery, because it is part of Jakarta Persistence and works with any provider.

4. Conclusion

The method em.createStoredProcedureQuery() calls a database procedure in a few lines. We register the parameters in the procedure’s order and run execute() or getResultList(). After that, we read the OUT values or the mapped entities. For repeated calls, @NamedStoredProcedureQuery or a Spring Data @Procedure method keeps the details in one place.

5. References

Happy Learning !!

Source Code on Github

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.