15.2 SQL Execution: `Statement`, `PreparedStatement`, and `CallableStatement`

Key Takeaways

  • The JDBC statement hierarchy consists of three interfaces: Statement (dynamic uncompiled SQL), PreparedStatement (precompiled parameterized SQL), and CallableStatement (stored procedure execution).

  • PreparedStatement prevents SQL injection by separating SQL syntax from user-supplied literal data using '?' parameter placeholders, while also enabling query plan caching on the database engine.

  • SQL execution methods have specific return contracts: executeQuery() returns a ResultSet (SELECT only), executeUpdate() returns an int (INSERT/UPDATE/DELETE/DDL), and execute() returns a boolean indicating whether the first result is a ResultSet.

  • Parameter placeholders in PreparedStatement and CallableStatement use 1-based indexing; attempting to set or retrieve index 0 throws a runtime SQLException.

  • CallableStatement handles stored procedure output by registering OUT and INOUT parameters before execution using registerOutParameter(index, java.sql.Types).

Last updated: October 2026

SQL Execution: Statement, PreparedStatement, and CallableStatement

Once a database connection is established, the application interacts with the database engine by sending SQL commands. The JDBC API provides three primary statement interfaces organized in an inheritance hierarchy:

             +-----------------------+
             |   java.sql.Statement   |
             +-----------------------+
                         ^
                         |
          +------------------------------+
          |  java.sql.PreparedStatement  |
          +------------------------------+
                         ^
                         |
          +------------------------------+
          |   java.sql.CallableStatement |
          +------------------------------+
  • Statement: Created via conn.createStatement(). Used for executing simple, static SQL queries without parameters. Susceptible to SQL injection if used with dynamic string concatenation.
  • PreparedStatement: Created via conn.prepareStatement(sql). Extends Statement. Precompiles SQL statements with parameter placeholders (?), providing security against SQL injection and performance gains through database query plan caching.
  • CallableStatement: Created via conn.prepareCall(sql). Extends PreparedStatement. Used to invoke database stored procedures and stored functions supporting IN, OUT, and INOUT parameters.

Statement vs. PreparedStatement and SQL Injection

Consider a user login authentication query implemented with a standard Statement:

// VULNERABLE IMPLEMENTATION - NEVER USE IN PRODUCTION!
String user = request.getParameter("user"); // Attacker inputs: admin' --
String pass = request.getParameter("pass");
String sql = "SELECT * FROM users WHERE username = '" + user + "' AND password = '" + pass + "'";

try (Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    if (rs.next()) {
        // User successfully authenticated without knowing the password!
    }
}

Because the raw SQL string is concatenated, the attacker's input alters the logical syntax of the query. The resulting command executes as:

SELECT * FROM users WHERE username = 'admin' --' AND password = '...'

The -- comment operator truncates the remainder of the query, bypassing authentication entirely.

The PreparedStatement Defense

PreparedStatement neutralizes SQL injection by transmitting the SQL template and the user values across separate protocol channels or escaping them internally:

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setString(1, user); // Bound strictly as literal data
    pstmt.setString(2, pass); // Cannot alter the parsed query tree
    try (ResultSet rs = pstmt.executeQuery()) {
        if (rs.next()) {
            // Safe authentication
        }
    }
}

When conn.prepareStatement(sql) is invoked, the database engine compiles the SQL string into an executable query plan. When setString() is called, the value is treated strictly as a literal data parameter, making syntax hijacking impossible.

Note

PreparedStatement also yields significant performance advantages in batch processing. When executing the same SQL statement repeatedly with varying arguments, the database reuses the precompiled query plan rather than repeatedly parsing, validating, and optimizing the SQL.


The Three Core Execution Methods

The JDBC statement interfaces provide three distinct execution methods. Choosing the incorrect execution method results in a runtime SQLException.

Execution MethodValid SQL StatementsReturn TypeContract & Behavior
executeQuery()SELECT queries onlyResultSetReturns a tabular ResultSet. Throws SQLException if invoked on DML (INSERT, UPDATE, DELETE) or DDL (CREATE, DROP).
executeUpdate()DML (INSERT, UPDATE, DELETE) or DDL (CREATE, ALTER, DROP)intReturns the number of database rows affected by DML, or 0 for DDL statements. Throws SQLException if invoked on a SELECT.
execute()Any SQL statement (dynamic queries or multi-result calls)booleanReturns true if the first result is a ResultSet; returns false if the first result is an update count or there are no results.

Working with execute()

The generic execute() method is used when the type of SQL statement is unknown at compile time or when executing commands that produce multiple result sets:

boolean isResultSet = stmt.execute(sqlString);

if (isResultSet) {
    try (ResultSet rs = stmt.getResultSet()) {
        while (rs.next()) {
            System.out.println("Data: " + rs.getString(1));
        }
    }
} else {
    int updateCount = stmt.getUpdateCount();
    System.out.println("Rows modified: " + updateCount);
}

Warning

The Overloaded Method Exam Trap Notice that Statement defines executeQuery(String sql), executeUpdate(String sql), and execute(String sql). Because PreparedStatement extends Statement, it inherits these methods. However, on a PreparedStatement, you must invoke the zero-argument versions: pstmt.executeQuery(), pstmt.executeUpdate(), and pstmt.execute(). Passing a SQL string to a PreparedStatement execution method (e.g., pstmt.executeQuery(sql)) overrides the precompiled statement or throws a runtime SQLException on compliant drivers!

Parameter Binding Rules and 1-Based Indexing

In PreparedStatement, parameters are designated with positional question mark placeholders (?). The JDBC API provides strongly typed setter methods for binding values:

  • setInt(int parameterIndex, int x)
  • setLong(int parameterIndex, long x)
  • setDouble(int parameterIndex, double x)
  • setString(int parameterIndex, String x)
  • setDate(int parameterIndex, java.sql.Date x)
  • setNull(int parameterIndex, int sqlType) (e.g., java.sql.Types.VARCHAR)

The 1-Based Indexing Rule

Unlike standard Java collections and arrays, which are 0-indexed, all JDBC parameter indices are 1-based:

String sql = "UPDATE employees SET salary = ?, department = ? WHERE emp_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setDouble(1, 85000.50);  // 1st placeholder: salary
    pstmt.setString(2, "ENGINEERING"); // 2nd placeholder: department
    pstmt.setInt(3, 1042);         // 3rd placeholder: emp_id

    int rowsAffected = pstmt.executeUpdate();
    System.out.println("Updated rows: " + rowsAffected);
}

If code attempts to bind index 0 (e.g., pstmt.setInt(0, 1042);) or an index exceeding the count of placeholders (e.g., pstmt.setInt(4, 1042);), the driver throws a runtime java.sql.SQLException: Parameter index out of range.


CallableStatement and Stored Procedures

Database stored procedures and functions encapsulate business logic inside the database engine. JDBC accesses them using CallableStatement via standard SQL escape syntax:

{call procedure_name(?, ?, ...)}
{? = call function_name(?, ...)}

Stored procedures support three parameter modes:

  1. IN: Supplies input data from Java to the stored procedure.
  2. OUT: Returns output data from the stored procedure to Java.
  3. INOUT: Supplies input data and receives updated output data through the same parameter.

Registering OUT Parameters

Before executing a CallableStatement, any OUT or INOUT parameter must be registered with its corresponding SQL data type from java.sql.Types. After execution, the values are retrieved using getter methods:

// Stored procedure signature: calculate_bonus(IN emp_id INT, OUT bonus DOUBLE, INOUT rating VARCHAR)
String callSql = "{call calculate_bonus(?, ?, ?)}";

try (CallableStatement cstmt = conn.prepareCall(callSql)) {
    // Parameter 1: IN parameter (emp_id)
    cstmt.setInt(1, 1042);

    // Parameter 2: OUT parameter (bonus) - Must be registered before execution!
    cstmt.registerOutParameter(2, java.sql.Types.DOUBLE);

    // Parameter 3: INOUT parameter (rating) - Bound as input AND registered as output!
    cstmt.setString(3, "EXCELLENT");
    cstmt.registerOutParameter(3, java.sql.Types.VARCHAR);

    // Execute the stored procedure
    cstmt.execute();

    // Retrieve the OUT results using 1-based parameter indices
    double bonus = cstmt.getDouble(2);
    String finalRating = cstmt.getString(3);

    System.out.println("Bonus: $" + bonus + ", Final Rating: " + finalRating);
}

Warning

If you attempt to invoke a getter method (such as cstmt.getDouble(2)) without first registering the parameter via registerOutParameter(), the JDBC driver throws a SQLException indicating an unregistered output parameter.

Test Your Knowledge

A developer executes an unknown dynamic SQL statement using the generic execute() method on a Statement instance:

boolean result = stmt.execute(dynamicSql);

If dynamicSql was an UPDATE statement that modified 12 rows in the database, what is the value of result, and how should the application retrieve the modification count?

A

result is false, and the count is retrieved by calling stmt.getGeneratedKeys().

B

result is false, and the count is retrieved by calling stmt.getUpdateCount().

C

result is true, and the count is retrieved by calling stmt.getResultSet().getInt(1).

D

result is true, and the count is retrieved by calling stmt.getInt(1).

Test Your Knowledge

Consider the following code snippet designed to update product prices:

String sql = "UPDATE products SET price = ? WHERE product_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setDouble(0, 19.99);
    pstmt.setInt(1, 501);
    int count = pstmt.executeUpdate();
    System.out.println("Updated: " + count);
}

What happens when this code is executed?

A

The code throws a runtime SQLException because JDBC parameter indices are 1-based, making index 0 invalid.

B

A compilation error occurs because PreparedStatement parameter indices must be configured using column name strings.

C

The code compiles and executes successfully, updating product 501 with price 19.99.

D

The code compiles, but executeUpdate() returns -1 because parameter 0 is reserved for internal driver state.

Test Your Knowledge

A developer is writing code to invoke a stored procedure that calculates an employee's annual pension. The procedure takes an employee ID as an input parameter and returns the calculated pension amount as an output parameter:

String call = "{call get_pension(?, ?)}";
try (CallableStatement cstmt = conn.prepareCall(call)) {
    cstmt.setInt(1, 4028);
    cstmt.registerOutParameter(2, java.sql.Types.DECIMAL);
    cstmt.execute();
    BigDecimal pension = cstmt.getBigDecimal(2);
    System.out.println("Pension: " + pension);
}

Which statement accurately describes the role and requirements of registerOutParameter in this code?

A

registerOutParameter must be called after cstmt.execute() so that the driver knows how to decode the database network buffer.

B

registerOutParameter binds the value 2 into parameter position 2 as an input argument before the procedure executes.

C

registerOutParameter must be invoked before cstmt.execute() to inform the driver of the expected SQL data type from java.sql.Types for the OUT parameter.

D

registerOutParameter is optional; JDBC drivers dynamically infer the output data type from the getBigDecimal() return type after execution.

Test Your Knowledge

Consider the following code snippet querying database records:

String sql = "SELECT id, name FROM customers WHERE region = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
    pstmt.setString(1, "NORTH");
    ResultSet rs = pstmt.executeQuery(sql); // Line X
    while (rs.next()) {
        System.out.println(rs.getString("name"));
    }
}

What is the flaw in Line X?

A

Calling executeQuery(String) on a PreparedStatement is not allowed, and compliant drivers throw a runtime SQLException.

B

Passing a SQL string to executeQuery() causes the driver to treat parameter 1 as a literal question mark character.

C

PreparedStatement does not support returning a ResultSet; conn.createStatement() must be used for all SELECT queries.

D

Line X causes a compilation error because executeQuery(String) does not exist on PreparedStatement or its superinterfaces.

Sections you finish are checked off in the contents.