☕ Java
CallableStatement
java.sql.CallableStatement extends PreparedStatement to invoke database stored procedures and stored functions through a standardized JDBC escape syntax, supporting IN parameters (values passed into the procedure), OUT parameters (values the procedure returns through a parameter rather than a result set), and INOUT parameters (which serve as both). This entry covers the call escape syntax, registering and retrieving OUT parameters, handling stored procedures that return multiple result sets, and the practical tradeoffs of pushing logic into stored procedures versus keeping it in application code.
The Call Escape Syntax and IN Parameters
CallableStatement objects are created with conn.prepareCall(sql), where the SQL string uses JDBC's standardized call escape syntax rather than ordinary SQL: {call procedure_name(?, ?, ?)} for a stored procedure, or {? = call function_name(?, ?)} for a stored function that returns a single value. This escape syntax is part of the JDBC specification itself and is translated by the driver into whatever native syntax the target database actually uses to invoke procedures (which varies considerably between vendors — PL/SQL's CALL syntax in Oracle differs from T-SQL's EXEC in SQL Server, for instance), which is precisely the abstraction the escape syntax exists to hide from application code.
For parameters that are purely inputs to the procedure (IN parameters, the default and most common case), CallableStatement is used exactly like PreparedStatement: the same setString(), setInt(), setBigDecimal(), and other typed setters bind values to the numbered '?' placeholders before execution. Many simple stored procedure calls — ones that only take input and perform an action, or take input and return a single result set — require nothing beyond this and the appropriate execute method, making them indistinguishable in usage from an ordinary PreparedStatement call aside from the {call ...} syntax itself.
Java
import java.sql.*;
public class CallableStatementInParams {
public static void main(String[] args) throws SQLException {
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/storedb", "app_user", "secret")) {
// ── Stored procedure with only IN parameters, no return value ──
// Underlying procedure signature (illustrative):
// PROCEDURE apply_discount(p_product_id IN BIGINT, p_percent IN NUMERIC)
try (CallableStatement cs = conn.prepareCall("{call apply_discount(?, ?)}")) {
cs.setLong(1, 42L);
cs.setBigDecimal(2, new java.math.BigDecimal("0.15"));
cs.execute();
}
// ── Stored function returning a single scalar value ────────────
// Underlying function signature (illustrative):
// FUNCTION calculate_tax(p_amount NUMERIC) RETURNS NUMERIC
try (CallableStatement cs = conn.prepareCall("{? = call calculate_tax(?)}")) {
cs.registerOutParameter(1, Types.NUMERIC); // the function's return value
cs.setBigDecimal(2, new java.math.BigDecimal("199.99"));
cs.execute();
java.math.BigDecimal tax = cs.getBigDecimal(1);
System.out.println("Tax: " + tax);
}
}
}
}OUT and INOUT Parameters
Stored procedures, unlike functions, don't have a single return value in the SQL sense — instead, they communicate results back to the caller through OUT parameters (write-only from the procedure's perspective; the caller never supplies an input value for these) or INOUT parameters (the caller supplies an initial value, and the procedure may modify it, with the final value retrievable after execution). For both kinds, the application must first call registerOutParameter(index, sqlType) before execution to tell the driver what SQL type to expect back at that parameter position — this registration is necessary even for INOUT parameters, in addition to also calling the normal setXxx() method to supply the input portion of an INOUT parameter.
After execute() completes, the OUT and INOUT values are retrieved using the same family of typed getter methods used on ResultSet — getString(index), getInt(index), getBigDecimal(index), and so on — but called on the CallableStatement itself rather than on a separate result object. Mixing up which parameters are IN, OUT, or INOUT relative to how the actual stored procedure was declared in the database produces driver- or database-specific errors that can be confusing to diagnose, since the JDBC API itself doesn't introspect the procedure's real signature — it simply trusts the application code's registerOutParameter() and setXxx() calls to match what the procedure actually expects.
Java
import java.sql.*;
import java.math.BigDecimal;
public class CallableStatementOutParams {
// Illustrative procedure signature:
// PROCEDURE process_refund(
// p_order_id IN BIGINT,
// p_amount INOUT NUMERIC, -- caller supplies requested amount,
// -- procedure may cap/adjust it
// p_new_status OUT VARCHAR -- procedure reports resulting status
// )
public static void processRefund(Connection conn, long orderId, BigDecimal requestedAmount)
throws SQLException {
String sql = "{call process_refund(?, ?, ?)}";
try (CallableStatement cs = conn.prepareCall(sql)) {
cs.setLong(1, orderId); // pure IN
cs.setBigDecimal(2, requestedAmount); // INOUT: input half
cs.registerOutParameter(2, Types.NUMERIC); // INOUT: output half
cs.registerOutParameter(3, Types.VARCHAR); // pure OUT
cs.execute();
BigDecimal actualAmount = cs.getBigDecimal(2); // possibly adjusted by the procedure
String status = cs.getString(3);
System.out.println("Refund processed: $" + actualAmount + " — status: " + status);
}
}
}Stored Procedures Returning Multiple Result Sets
Some stored procedures, particularly in databases like SQL Server and Sybase, can return more than one result set from a single call — for example, a procedure that returns both a summary row and a detail table. JDBC handles this through execute() (rather than executeQuery(), which assumes exactly one result set) followed by getResultSet() to retrieve the first result set and getMoreResults() to advance to the next one, which returns true if another result set is available and false once they're exhausted; getMoreResults() also accepts a constant controlling whether the previously retrieved ResultSet should be closed automatically when advancing, which matters for memory management if earlier result sets are large.
Not every database or driver supports multiple result sets from a single stored procedure call equally well — PostgreSQL, for instance, more commonly models multiple result sets through cursor-returning functions accessed somewhat differently than the straightforward getMoreResults() loop that works cleanly against SQL Server — so this pattern should be tested specifically against the target database rather than assumed to be universally portable. For a procedure that interleaves OUT parameters with result sets, the OUT parameter values are generally only safe to read after all result sets have been fully processed and getMoreResults() has returned false, since drivers commonly finalize the call's OUT parameter values only once the entire stored procedure response has been consumed.
Java
import java.sql.*;
public class CallableStatementMultipleResultSets {
// Illustrative procedure (SQL Server-style) returning two result sets:
// a summary row, then a detail table.
public static void runReportProcedure(Connection conn, long reportId) throws SQLException {
try (CallableStatement cs = conn.prepareCall("{call generate_report(?)}")) {
cs.setLong(1, reportId);
boolean hasResultSet = cs.execute();
while (hasResultSet) {
try (ResultSet rs = cs.getResultSet()) {
ResultSetMetaData meta = rs.getMetaData();
System.out.println("--- Result set with " + meta.getColumnCount() + " columns ---");
while (rs.next()) {
StringBuilder row = new StringBuilder();
for (int i = 1; i <= meta.getColumnCount(); i++) {
row.append(rs.getString(i)).append(" ");
}
System.out.println(row);
}
}
// Advance to the next result set; false means none remain
hasResultSet = cs.getMoreResults();
}
}
}
}Related Topics in Java Database Connectivity (JDBC)
JDBC Architecture
JDBC (Java Database Connectivity) is the standard Java API for connecting to and executing operations against relational databases, defined as a set of interfaces in java.sql and javax.sql that database vendors implement with concrete driver classes. Its architecture is deliberately layered to decouple application code from any specific database product: applications code against interfaces like Connection, Statement, and ResultSet, while a pluggable driver underneath translates those calls into the vendor-specific network protocol the database actually understands. This entry covers the two-layer (API/driver) architecture, the four historical driver types, the role of the JDBC driver manager and connection pooling layers, and how the pieces fit together at runtime.
JDBC Drivers
A JDBC driver is the vendor-specific library that implements the java.sql/javax.sql interfaces for a particular database product, translating standard JDBC calls into that database's native network protocol. Choosing, loading, registering, and configuring the correct driver is a prerequisite for any JDBC code to run, and the mechanics of how a driver gets discovered and instantiated have evolved significantly since JDBC's introduction. This entry covers driver registration via the Service Provider Interface (SPI) versus the older explicit Class.forName() approach, JDBC URL syntax and structure, driver-specific configuration properties, and how to choose and manage driver dependencies in a real project.
DriverManager
java.sql.DriverManager is the original, classic entry point into JDBC: a static factory class that maintains a registry of available Driver implementations and hands back a live Connection when given a JDBC URL and credentials. It predates the javax.sql.DataSource abstraction and connection pooling, and while it remains fully functional and is still commonly used for simple applications, scripts, and tests, production server-side code generally prefers DataSource-based connection pooling for the reasons covered below. This entry covers how DriverManager locates and selects a driver, its core methods, login timeout and logging configuration, and where it fits (and doesn't fit) in modern application architecture.
Connection
java.sql.Connection represents a single session with a specific database — the live link through which all statements, transactions, and metadata queries flow, and the factory from which Statement, PreparedStatement, and CallableStatement objects are created. A Connection is a relatively expensive, stateful resource that wraps an underlying network socket and server-side session, which is why its lifecycle, transaction settings, and proper closing matter so much in practice. This entry covers obtaining and closing connections, transaction control (auto-commit, commit, rollback, savepoints, isolation levels), creating statements from a connection, and connection metadata and health checks.