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.
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
ResultSet.TYPE_FORWARD_ONLY(Default):- The cursor moves exclusively forward row by row using
next(). - Offers maximum performance and minimal memory overhead.
- The cursor moves exclusively forward row by row using
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
ResultSetis open are not visible.
- The cursor can navigate bidirectionally (
ResultSet.TYPE_SCROLL_SENSITIVE:- The cursor is scrollable bidirectionally.
- The data view reflects dynamic changes made to underlying rows by other transactions while the
ResultSetremains open (subject to driver and database engine capabilities).
Result Set Concurrency
ResultSet.CONCUR_READ_ONLY(Default): Rows cannot be updated through theResultSet.ResultSet.CONCUR_UPDATABLE: Rows can be modified, deleted, or inserted directly usingResultSetmethods (such asupdateString(),updateRow(), andinsertRow()).
Navigating Scrollable Result Sets
When a ResultSet is scrollable (TYPE_SCROLL_INSENSITIVE or TYPE_SCROLL_SENSITIVE), the cursor can be repositioned arbitrarily:
| Method | Movement Description | Return 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 Javanull. - 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
autoCommitistruethrows a runtimeSQLException. - 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()orconn.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
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?
amount is 0, and Line X should be: if (rs.isNull("scholarship"))
amount is null, and Line X should be: if (amount == null)
amount is -1, and Line X should be: if (amount == -1)
amount is 0, and Line X should be: if (rs.wasNull())
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?
The cursor is before the first row, and hasPrev is false.
The cursor is on row 2, and hasPrev is true.
The cursor is on row 1, and hasPrev is false.
The cursor is on row 4, and hasPrev is true.
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?
Both Operation 1 and Operation 2 are rolled back completely because no-argument conn.rollback() reverts the entire uncommitted transaction regardless of intermediate savepoints.
The database automatically keeps Operation 1 uncommitted in memory and retries Operation 2 upon the next connection open.
Line X throws a SQLException because an unhandled savepoint requires invoking conn.rollback(sp) instead of no-argument rollback().
Operation 1 is permanently committed while Operation 2 is rolled back because the savepoint protects preceding statements.
What happens when an application executes rs.previous() on a ResultSet created via the default conn.createStatement().executeQuery("SELECT * FROM items") call?
A SQLException is thrown at runtime because default result sets are TYPE_FORWARD_ONLY, which forbids backward cursor traversal.
The cursor moves backward to the previous row because modern JDBC drivers default to scrollable result sets.
A compilation error occurs because previous() is only defined on the ScrollableResultSet subinterface.
rs.previous() returns false without moving the cursor or throwing an exception.
Sections you finish are checked off in the contents.