15.3 `ResultSet` Navigation, Data Types, and Transaction Management (Commit, Rollback, Savepoints)

Key Takeaways

  • A ResultSet cursor starts positioned before the first row (beforeFirst); calling next() advances the cursor and returns true if a row is present, or false when the end of the result set is reached.

  • Default result sets are TYPE_FORWARD_ONLY and CONCUR_READ_ONLY; scrollable result sets (TYPE_SCROLL_INSENSITIVE or TYPE_SCROLL_SENSITIVE) allow bidirectional navigation via previous(), first(), last(), and absolute().

  • Primitive getter methods (such as getInt and getDouble) return 0 or false when reading SQL NULL; applications must call wasNull() immediately after reading the column to differentiate an actual 0 from SQL NULL.

  • Transactions are managed by disabling auto-commit via connection.setAutoCommit(false), grouping multiple operations atomically, and concluding with connection.commit() or connection.rollback().

  • Savepoints allow fine-grained, intermediate rollback checkpoints within a transaction via connection.setSavepoint() and connection.rollback(savepoint) without discarding preceding operations.

Last updated: October 2026

ResultSet Navigation, Data Types, and Transaction Management

A ResultSet represents a tabular data stream returned by executing a database query. Navigating this stream, converting database columns to Java types, handling SQL NULL values, and controlling transaction boundaries are core competencies for database programming in Java.


ResultSet Cursor Architecture

A ResultSet maintains a cursor pointing to its current row of data. When a query executes, the cursor is initially positioned before the first row (the beforeFirst position):

 Cursor Position      Row Index      Data Row
+------------------+---------------+----------------------------+
| [beforeFirst]    |      0        | (Initial state before next)|
+------------------+---------------+----------------------------+
| rs.next() -> true|      1        | ID: 101, Name: "Alice"     |
+------------------+---------------+----------------------------+
| rs.next() -> true|      2        | ID: 102, Name: "Bob"       |
+------------------+---------------+----------------------------+
| rs.next() -> true|      3        | ID: 103, Name: "Charlie"   |
+------------------+---------------+----------------------------+
| rs.next() ->false|  [afterLast]  | (No more rows available)   |
+------------------+---------------+----------------------------+

To read data, you must advance the cursor by invoking rs.next(). If a row exists, next() positions the cursor on that row and returns true. When no more rows remain, it moves past the last row (the afterLast position) and returns false:

String sql = "SELECT id, name, balance FROM accounts";
try (Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    while (rs.next()) {
        // Cursor is on a valid row
        int id = rs.getInt("id");
        String name = rs.getString("name");
        double balance = rs.getDouble("balance");
        System.out.println(id + ": " + name + " ($ " + balance + ")");
    }
    // rs.next() returned false; cursor is now afterLast
}

Warning

The "Before First" Trap Attempting to read a column before calling rs.next() (or after rs.next() returns false) throws a java.sql.SQLException: Invalid cursor state or Current position is not a valid row. Always call rs.next() at least once before accessing data.

Cursor Types and Concurrency

By default, a ResultSet is forward-only and read-only. You can request advanced scrolling and concurrency capabilities when creating the statement:

Statement stmt = conn.createStatement(resultSetType, resultSetConcurrency);
PreparedStatement pstmt = conn.prepareStatement(sql, resultSetType, resultSetConcurrency);

Result Set Types

  1. ResultSet.TYPE_FORWARD_ONLY (Default):
    • The cursor moves exclusively forward row by row using next().
    • Offers maximum performance and minimal memory overhead.
  2. ResultSet.TYPE_SCROLL_INSENSITIVE:
    • The cursor can navigate bidirectionally (previous(), first(), last(), absolute(), relative()).
    • The data view is a static snapshot; changes made to the underlying database by concurrent transactions while the ResultSet is open are not visible.
  3. ResultSet.TYPE_SCROLL_SENSITIVE:
    • The cursor is scrollable bidirectionally.
    • The data view reflects dynamic changes made to underlying rows by other transactions while the ResultSet remains open (subject to driver and database engine capabilities).

Result Set Concurrency

  1. ResultSet.CONCUR_READ_ONLY (Default): Rows cannot be updated through the ResultSet.
  2. ResultSet.CONCUR_UPDATABLE: Rows can be modified, deleted, or inserted directly using ResultSet methods (such as updateString(), updateRow(), and insertRow()).

Navigating Scrollable Result Sets

When a ResultSet is scrollable (TYPE_SCROLL_INSENSITIVE or TYPE_SCROLL_SENSITIVE), the cursor can be repositioned arbitrarily:

MethodMovement DescriptionReturn Type
next()Moves cursor forward one row.boolean (true if on a valid row)
previous()Moves cursor backward one row.boolean (true if on a valid row)
first()Moves cursor directly to the first row.boolean (false if result set is empty)
last()Moves cursor directly to the last row.boolean (false if result set is empty)
beforeFirst()Moves cursor before the first row (initial state).void
afterLast()Moves cursor after the last row.void
absolute(int row)Moves cursor to the given row number. absolute(1) is first row; absolute(-1) is last row; absolute(0) moves before first row.boolean (true if on a valid row)
relative(int rows)Moves cursor relative to current position. relative(2) advances 2 rows; relative(-1) moves back 1 row.boolean (true if on a valid row)
try (Statement stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
     ResultSet rs = stmt.executeQuery("SELECT name FROM players ORDER BY score DESC")) {
    if (rs.last()) {
        System.out.println("Lowest scorer (last row): " + rs.getString("name"));
        int totalRows = rs.getRow(); // Returns current row index
        System.out.println("Total players: " + totalRows);
    }
    if (rs.first()) {
        System.out.println("Highest scorer (first row): " + rs.getString("name"));
    }
}

Column Retrieval and the wasNull() Check

Columns can be retrieved using either a 1-based column index (rs.getInt(1)) or a case-insensitive column label (rs.getString("username")).

int id = rs.getInt(1);             // 1-based numeric index
String name = rs.getString("name"); // Column label from SELECT list

The SQL NULL Dilemma and wasNull()

In SQL, columns can hold NULL. When reading nullable columns in JDBC:

  • Object getter methods (getString(), getDate(), getBigDecimal()) return Java null.
  • Primitive getter methods (getInt(), getDouble(), getBoolean(), getLong()) cannot return null. Instead, they return their primitive default values (0, 0.0, false).

If rs.getInt("bonus") returns 0, you cannot determine whether the employee had a bonus of $0 or if the database value was NULL. To resolve this ambiguity, JDBC provides the wasNull() method on ResultSet:

int bonus = rs.getInt("bonus");
if (rs.wasNull()) {
    System.out.println("Bonus is NOT ASSIGNED (SQL NULL)");
} else {
    System.out.println("Bonus earned: $" + bonus);
}

Warning

The wasNull() Invocation Order Rule wasNull() checks whether the last column read on the ResultSet was SQL NULL. You must call the getter method first, and then immediately inspect rs.wasNull(). Calling wasNull() before reading any column produces undefined or driver-dependent behavior.

Transaction Management: Auto-Commit, Commit, and Rollback

By default, a new JDBC Connection operates in auto-commit mode (conn.getAutoCommit() == true). Under auto-commit, every individual SQL statement is treated as its own independent transaction that commits immediately upon execution.

To group multiple operations into an atomic business unit (where either all operations succeed or all are reverted), you must explicitly disable auto-commit:

public void transferFunds(Connection conn, int fromAcc, int toAcc, double amount) throws SQLException {
    boolean originalAutoCommit = conn.getAutoCommit();
    try {
        conn.setAutoCommit(false); // Begin explicit transaction boundary

        try (PreparedStatement debit = conn.prepareStatement(
                "UPDATE accounts SET balance = balance - ? WHERE id = ?");
             PreparedStatement credit = conn.prepareStatement(
                "UPDATE accounts SET balance = balance + ? WHERE id = ?")) {

            debit.setDouble(1, amount);
            debit.setInt(2, fromAcc);
            debit.executeUpdate();

            credit.setDouble(1, amount);
            credit.setInt(2, toAcc);
            credit.executeUpdate();

            conn.commit(); // Atomically persist all modifications
            System.out.println("Transfer successful!");
        }
    } catch (SQLException e) {
        conn.rollback(); // Revert all modifications on failure
        throw e;
    } finally {
        conn.setAutoCommit(originalAutoCommit); // Restore original connection state
    }
}

Intermediate Checkpoints with Savepoint

A Savepoint provides fine-grained control within a transaction, allowing an application to roll back a subset of operations without aborting the entire transaction.

conn.setAutoCommit(false);
try {
    // Step 1: Create primary order
    orderStmt.executeUpdate();

    // Establish intermediate checkpoint
    Savepoint sp = conn.setSavepoint("OrderCreated");

    try {
        // Step 2: Attempt optional inventory reservation
        inventoryStmt.executeUpdate();
    } catch (SQLException e) {
        System.err.println("Inventory reservation failed; rolling back to checkpoint");
        conn.rollback(sp); // Reverts Step 2 while preserving Step 1!
    }

    // Step 3: Complete order processing
    paymentStmt.executeUpdate();

    conn.commit(); // Persists Step 1, rolled-back Step 2 state, and Step 3
} catch (SQLException e) {
    conn.rollback(); // Global rollback if primary steps fail
}

Savepoint Rules

  • Setting a savepoint when autoCommit is true throws a runtime SQLException.
  • Invoking conn.rollback(savepoint) reverts only those operations executed after that savepoint was established.
  • Savepoints can be explicitly released using conn.releaseSavepoint(savepoint) to free server resources.
  • Calling no-argument conn.commit() or conn.rollback() automatically releases and invalidates all active savepoints.

Proper Closing Order for JDBC Resources

JDBC resources consume both client memory and server-side cursors and locks. They should always be closed in reverse order of creation:

ResultSet ---> Statement ---> Connection

Using nested or multi-resource try-with-resources statements ensures clean, automatic closing in this exact order:

try (Connection conn = dataSource.getConnection();
     PreparedStatement pstmt = conn.prepareStatement(query);
     ResultSet rs = pstmt.executeQuery()) {
    while (rs.next()) {
        // process data
    }
} // rs closes first, then pstmt, then conn
Test Your Knowledge

A database table students has a nullable integer column named scholarship. For student ID 204, the scholarship value is SQL NULL. A developer executes the following code:

if (rs.next()) {
    int amount = rs.getInt("scholarship");
    // Line X: verify if the database value was NULL
}

What is the value of the amount variable, and which code at Line X correctly determines if the column in the database was NULL?

A

amount is 0, and Line X should be: if (rs.isNull("scholarship"))

B

amount is null, and Line X should be: if (amount == null)

C

amount is -1, and Line X should be: if (amount == -1)

D

amount is 0, and Line X should be: if (rs.wasNull())

Test Your Knowledge

A scrollable ResultSet created with TYPE_SCROLL_INSENSITIVE contains exactly 5 rows. Consider the following sequence of cursor navigation calls:

rs.last();                       // Line 1
rs.relative(-2);                 // Line 2
boolean hasPrev = rs.previous(); // Line 3

On which row number is the cursor positioned after Line 3, and what is the value of hasPrev?

A

The cursor is before the first row, and hasPrev is false.

B

The cursor is on row 2, and hasPrev is true.

C

The cursor is on row 1, and hasPrev is false.

D

The cursor is on row 4, and hasPrev is true.

Test Your Knowledge

A developer writes a fund transfer routine with an intermediate savepoint:

conn.setAutoCommit(false);
try {
    debitStmt.executeUpdate();                    // Operation 1
    Savepoint sp = conn.setSavepoint("DebitDone"); // Checkpoint
    creditStmt.executeUpdate();                   // Operation 2 (Throws SQLException)
    conn.commit();
} catch (SQLException e) {
    conn.rollback();                              // Line X
}

What happens to the database state when Operation 2 throws a SQLException and Line X executes?

A

Both Operation 1 and Operation 2 are rolled back completely because no-argument conn.rollback() reverts the entire uncommitted transaction regardless of intermediate savepoints.

B

The database automatically keeps Operation 1 uncommitted in memory and retries Operation 2 upon the next connection open.

C

Line X throws a SQLException because an unhandled savepoint requires invoking conn.rollback(sp) instead of no-argument rollback().

D

Operation 1 is permanently committed while Operation 2 is rolled back because the savepoint protects preceding statements.

Test Your Knowledge

What happens when an application executes rs.previous() on a ResultSet created via the default conn.createStatement().executeQuery("SELECT * FROM items") call?

A

A SQLException is thrown at runtime because default result sets are TYPE_FORWARD_ONLY, which forbids backward cursor traversal.

B

The cursor moves backward to the previous row because modern JDBC drivers default to scrollable result sets.

C

A compilation error occurs because previous() is only defined on the ScrollableResultSet subinterface.

D

rs.previous() returns false without moving the cursor or throwing an exception.

Sections you finish are checked off in the contents.