☕ Java
ResultSet
java.sql.ResultSet represents the tabular result of a SQL query, exposing a cursor that starts positioned before the first row and is advanced one row at a time via next(), with typed getter methods to read column values out of the current row. Beyond basic iteration, ResultSet supports scrollable and updatable cursors, column metadata inspection, and NULL handling semantics that differ from ordinary Java reference/primitive distinctions. This entry covers cursor navigation and reading values, scrollability and updatability, and metadata plus NULL-handling pitfalls.
Cursor Navigation and Reading Column Values
A ResultSet's cursor begins positioned immediately before the first row, not on it, which is why next() must be called at least once before any column can be read — calling a getter before the first next() call, or after next() has returned false (meaning the cursor has moved past the last row), throws a SQLException. next() returns true if the cursor successfully moved to a row with data and false once rows are exhausted, making a while (rs.next()) { ... } loop the standard idiom for processing every row in order.
Column values can be retrieved either by 1-based column index or by column label (the column name, or its alias if the query used one), with the label-based overloads being generally preferred in application code because they remain correct if columns are reordered or added to the underlying query, whereas index-based access silently breaks or returns the wrong value if the SELECT clause's column order changes. JDBC provides a typed getter for essentially every SQL data type — getString(), getInt(), getLong(), getBigDecimal(), getBoolean(), getDate(), getTimestamp(), getBytes() for binary data, and getObject() as a generic fallback that returns whatever Java type the driver considers the natural mapping for that column's SQL type. Calling a getter whose Java type doesn't sensibly correspond to the actual column's SQL type either throws an exception or triggers a (sometimes lossy) conversion, depending on the driver, so matching getter to column type is worth being deliberate about rather than reaching for getObject() everywhere out of convenience.
Java
import java.sql.*;
public class ResultSetBasics {
public static void main(String[] args) throws SQLException {
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/storedb", "app_user", "secret");
PreparedStatement ps = conn.prepareStatement(
"SELECT id, name, price, in_stock, created_at FROM products WHERE category = ?")) {
ps.setString(1, "electronics");
try (ResultSet rs = ps.executeQuery()) {
// Cursor starts BEFORE the first row — next() must be called first
while (rs.next()) {
// Label-based access — preferred, resilient to column reordering
long id = rs.getLong("id");
String name = rs.getString("name");
java.math.BigDecimal price = rs.getBigDecimal("price");
boolean inStock = rs.getBoolean("in_stock");
Timestamp createdAt = rs.getTimestamp("created_at");
System.out.printf("%d: %s — $%.2f — stock=%b — created=%s%n",
id, name, price, inStock, createdAt);
}
}
// Index-based access — works, but breaks silently if SELECT column
// order changes; generally avoided in favor of label-based access
try (ResultSet rs = ps.executeQuery()) {
if (rs.next()) {
long idByIndex = rs.getLong(1); // 1-based, matches SELECT order above
String nameByIndex = rs.getString(2);
}
}
}
}
}Scrollability and Updatability
By default, a ResultSet is TYPE_FORWARD_ONLY (the cursor can only move forward via next(), never backward or to an arbitrary row) and CONCUR_READ_ONLY (rows cannot be modified through the ResultSet itself). These defaults are the lightest-weight and most widely supported option across drivers and databases, and are sufficient for the overwhelming majority of application code, which simply reads a query's results once from start to end.
When random access or backward movement is genuinely needed, Connection's overloaded createStatement() and prepareStatement() methods accept a result set type constant — TYPE_SCROLL_INSENSITIVE (a scrollable cursor that doesn't reflect changes made to the underlying data by other transactions after the result set was produced) or TYPE_SCROLL_SENSITIVE (scrollable and, to whatever extent the driver supports it, reflective of concurrent changes) — alongside a concurrency constant, CONCUR_UPDATABLE, which permits calling updateXxx() methods (updateString(), updateInt(), etc.) on the current row followed by updateRow() to push those changes back to the database, or deleteRow() to delete the current row directly through the ResultSet, or insertRow() after moving to the special insert row returned by moveToInsertRow(). Scrollable and updatable result sets carry meaningfully more overhead and are not supported identically by every driver/database combination, so they are best reserved for cases — such as certain desktop or grid-editing UI patterns — where the convenience of in-place row navigation and mutation genuinely outweighs simply issuing a separate, explicit UPDATE statement via PreparedStatement, which remains the more common and more portable approach for modifying data.
Java
import java.sql.*;
public class ResultSetScrollableUpdatable {
public static void main(String[] args) throws SQLException {
try (Connection conn = DriverManager.getConnection(
"jdbc:postgresql://localhost:5432/storedb", "app_user", "secret")) {
// ── Scrollable, read-only: random access without modification ──
try (Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
ResultSet rs = stmt.executeQuery("SELECT id, name FROM products ORDER BY id")) {
if (rs.last()) { // jump straight to the last row
System.out.println("Last product id: " + rs.getLong("id"));
}
rs.beforeFirst(); // reset to before the first row
if (rs.relative(3)) { // move forward 3 rows from current position
System.out.println("Third product: " + rs.getString("name"));
}
}
// ── Scrollable AND updatable: modify rows directly through the cursor ──
try (Statement stmt = conn.createStatement(
ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet rs = stmt.executeQuery("SELECT id, name, price FROM products WHERE id = 42")) {
if (rs.next()) {
rs.updateBigDecimal("price", new java.math.BigDecimal("24.99"));
rs.updateRow(); // pushes the change back to the database
}
rs.moveToInsertRow();
rs.updateString("name", "New Widget");
rs.updateBigDecimal("price", new java.math.BigDecimal("9.99"));
rs.insertRow(); // inserts a brand-new row via the ResultSet itself
rs.moveToCurrentRow();
}
}
}
}Metadata and NULL-Handling Pitfalls
Calling rs.getMetaData() returns a ResultSetMetaData object describing the shape of the result set itself — the number of columns, each column's name/label, declared SQL type, and nullability — which is useful for writing generic code that processes arbitrary queries without knowing their column structure ahead of time (database browsing tools, generic export utilities, ORMs). getColumnCount() gives the total number of columns, and getColumnName()/getColumnLabel()/getColumnType() are then called with a 1-based column index to describe each one individually.
A frequent source of subtle bugs is SQL NULL versus Java primitive defaults: when a getter for a primitive type (getInt(), getLong(), getDouble(), getBoolean()) is called on a column whose actual value is SQL NULL, JDBC does not throw an exception — it silently returns the primitive's default value (0 for getInt()/getLong(), 0.0 for getDouble(), false for getBoolean()), which is indistinguishable from an actual stored 0 or false unless checked for explicitly. The correct way to detect this is to call rs.wasNull() immediately after the getter call, which reports whether the just-retrieved value was actually SQL NULL; alternatively, using the corresponding wrapper-type getter (getObject(column, Integer.class) on modern drivers, or simply checking getObject(column) == null) sidesteps the ambiguity entirely for columns where NULL is expected to occur, since reference types can represent null directly rather than falling back to a primitive default.
Java
import java.sql.*;
public class ResultSetMetadataAndNulls {
public static void describeAnyQuery(Connection conn, String sql) throws SQLException {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
ResultSetMetaData meta = rs.getMetaData();
int columnCount = meta.getColumnCount();
for (int i = 1; i <= columnCount; i++) {
System.out.printf("Column %d: %s (%s), nullable=%s%n",
i, meta.getColumnLabel(i), meta.getColumnTypeName(i),
meta.isNullable(i) == ResultSetMetaData.columnNullable);
}
}
}
public static void demonstrateNullPitfall(Connection conn) throws SQLException {
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT discount_percent FROM products WHERE id = 42")) {
if (rs.next()) {
int discount = rs.getInt("discount_percent");
// BUG-PRONE: if discount_percent is SQL NULL, discount is silently 0 —
// indistinguishable from an actual stored value of 0 without checking wasNull()
if (rs.wasNull()) {
System.out.println("No discount set (column was NULL)");
} else {
System.out.println("Discount: " + discount + "%");
}
// Alternative: use the Object-returning getter, where null IS
// distinguishable directly without a follow-up check
Integer discountObj = rs.getObject("discount_percent", Integer.class);
if (discountObj == null) {
System.out.println("No discount set (via getObject)");
}
}
}
}
}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.