☕ Java

RowSet

javax.sql.RowSet extends ResultSet with JavaBean-style properties and event-listener support, and its most commonly used subtype, CachedRowSet, additionally disconnects entirely from the database after populating itself — holding all retrieved rows in memory and allowing the underlying Connection to be closed while the data remains accessible, modifiable, and even reconnectable for writing changes back later. This makes RowSet useful for scenarios where a result set's data needs to outlive the connection that produced it, or needs to be passed across layers of an application without dragging a live database connection along with it. This entry covers the RowSet interface family, CachedRowSet's disconnected operation model, and the practical tradeoffs versus simply copying ResultSet data into plain Java objects.

The RowSet Interface Family

javax.sql.RowSet itself extends ResultSet, adding JavaBean conventions (getters/setters for its own configuration, like the SQL command and connection parameters, rather than for the data it holds) and a listener mechanism (RowSetListener) that lets other code be notified when rows are inserted, updated, or deleted within the RowSet. Several specialized subinterfaces build on this base for different use cases: JdbcRowSet is a thin wrapper that stays connected to the database at all times, behaving close to an ordinary scrollable, updatable ResultSet but with the added JavaBean/event support; CachedRowSet (by far the most commonly used) reads data once and then disconnects, holding everything in memory; FilteredRowSet and JoinRowSet (in the optional javax.sql.rowset package) support filtering and joining operations on already-cached data without going back to the database; and WebRowSet extends CachedRowSet with the ability to serialize its contents to and from XML, intended for scenarios like passing tabular data across a network boundary in a self-describing format. Instances of these RowSet types are obtained through RowSetProvider.newFactory(), which returns a RowSetFactory whose createCachedRowSet(), createJdbcRowSet(), and similar methods construct the concrete implementation — this indirection exists for the same reason DriverManager exists for Connection: it decouples application code from any specific vendor's concrete RowSet implementation class, with the JDK supplying a default implementation (com.sun.rowset's classes, historically) unless a different provider is configured.
Java
import javax.sql.RowSetFactory;
import javax.sql.RowSetProvider;
import javax.sql.rowset.CachedRowSet;
import javax.sql.rowset.JdbcRowSet;

public class RowSetFamilyOverview {
    public static void main(String[] args) throws Exception {
        RowSetFactory factory = RowSetProvider.newFactory();

        // JdbcRowSet — stays connected, JavaBean-style configuration + event listeners
        JdbcRowSet jdbcRowSet = factory.createJdbcRowSet();
        jdbcRowSet.setUrl("jdbc:postgresql://localhost:5432/storedb");
        jdbcRowSet.setUsername("app_user");
        jdbcRowSet.setPassword("secret");
        jdbcRowSet.setCommand("SELECT id, name, price FROM products WHERE category = ?");
        jdbcRowSet.setString(1, "electronics");
        jdbcRowSet.execute();
        while (jdbcRowSet.next()) {
            System.out.println(jdbcRowSet.getString("name"));
        }
        jdbcRowSet.close();

        // CachedRowSet — the commonly used disconnected variant, covered in depth below
        CachedRowSet cachedRowSet = factory.createCachedRowSet();
        cachedRowSet.setUrl("jdbc:postgresql://localhost:5432/storedb");
        cachedRowSet.setUsername("app_user");
        cachedRowSet.setPassword("secret");
        cachedRowSet.setCommand("SELECT id, name, price FROM products WHERE category = ?");
        cachedRowSet.setString(1, "electronics");
        cachedRowSet.execute();  // connects, runs the query, populates in-memory rows, disconnects
        // at this point the underlying database connection is already closed
    }
}

CachedRowSet's Disconnected Operation Model

CachedRowSet's defining characteristic is that calling execute() performs the entire connect-query-populate-disconnect cycle internally: it opens its own connection using whatever URL/credentials were configured (or accepts an existing Connection passed in), runs the configured command, reads every row of the result into memory, and then closes the connection before execute() even returns — after which the CachedRowSet behaves like a self-contained, scrollable, updatable data structure that no longer depends on any live database connection at all. This is genuinely useful when retrieved data needs to be handed off to a different layer or even a different machine that shouldn't be holding a database connection open, or when a connection-constrained resource (a limited connection pool, a mobile/embedded client with intermittent connectivity) needs to be released as soon as possible after the data is fetched, with the data continuing to be used afterward. Modifications made to a CachedRowSet's rows (via the same updateXxx()/updateRow()/insertRow()/deleteRow() methods ResultSet itself defines) are tracked locally in memory and are not sent to the database automatically — writing them back requires calling acceptChanges(), either with an explicit Connection argument or relying on the CachedRowSet to reconnect using its originally configured connection parameters. This reconnect-to-write-back step introduces a real risk of conflicting with changes made by other transactions in the time between the original disconnect and the later acceptChanges() call, since the row data being written back reflects a potentially stale view of the database — CachedRowSet provides some conflict-detection support during acceptChanges(), but applications relying heavily on this disconnected edit-then-reconnect pattern need to think carefully about what should happen when the data has moved on underneath them.
Java
import javax.sql.RowSetFactory;
import javax.sql.RowSetProvider;
import javax.sql.rowset.CachedRowSet;
import java.sql.Connection;
import java.sql.DriverManager;

public class CachedRowSetDisconnectedExample {
    public static CachedRowSet fetchProductsDisconnected(String category) throws Exception {
        RowSetFactory factory = RowSetProvider.newFactory();
        CachedRowSet rowSet = factory.createCachedRowSet();

        rowSet.setUrl("jdbc:postgresql://localhost:5432/storedb");
        rowSet.setUsername("app_user");
        rowSet.setPassword("secret");
        rowSet.setCommand("SELECT id, name, price FROM products WHERE category = ?");
        rowSet.setString(1, category);

        rowSet.execute();
        // The connection used above is already closed by the time execute() returns.
        // 'rowSet' can now be passed to other code, held for a while, or even
        // serialized (CachedRowSet implements Serializable) without any live
        // database connection being held open in the meantime.
        return rowSet;
    }

    public static void modifyAndWriteBack(CachedRowSet rowSet) throws Exception {
        // Edits are tracked in memory only, against the disconnected row set
        if (rowSet.next()) {
            rowSet.updateBigDecimal("price", new java.math.BigDecimal("24.99"));
            rowSet.updateRow();
        }

        // Writing changes back requires reconnecting — either implicitly, using
        // the rowSet's own stored connection parameters, or explicitly:
        try (Connection conn = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/storedb", "app_user", "secret")) {
            rowSet.acceptChanges(conn);
            // Conflicts with changes made by other transactions since the original
            // disconnect are possible here and should be anticipated and handled.
        }
    }
}

RowSet vs. Plain Data Transfer Objects — Practical Tradeoffs

An alternative to using RowSet at all is simply iterating a regular ResultSet once, copying each row's values into a plain Java object (a record or simple class designed for that specific query's shape), and closing the ResultSet and Connection immediately afterward — letting the rest of the application work with ordinary, type-safe Java objects rather than a generic, ResultSet-like structure. This plain-object approach is overwhelmingly more common in modern application code, including virtually all code built on top of an ORM or a lightweight mapping library (Spring's JdbcTemplate with a RowMapper, for instance, does exactly this row-to-object copying under the hood), because it produces simpler, more type-safe, and more testable code than passing around a generic RowSet whose column access is still by index or string name rather than a named Java field. RowSet, and CachedRowSet in particular, remains genuinely useful in narrower situations: when code generically needs to handle arbitrary, not-statically-known query shapes while still wanting scrollability, updatability, and disconnected operation (some reporting tools and legacy desktop database-browsing applications fit this profile); when XML serialization of tabular data via WebRowSet is specifically wanted; or when integrating with older codebases and libraries already built around the RowSet API. For new application code with a known, fixed query shape — the large majority of cases — converting ResultSet rows into purpose-built Java objects as early as possible, and closing the Connection promptly afterward, is generally the simpler and more idiomatic choice over reaching for RowSet.
Java
import java.sql.*;
import java.util.ArrayList;
import java.util.List;

public class PlainObjectAlternativeToRowSet {
    record Product(long id, String name, java.math.BigDecimal price) {}

    // ── Plain-object approach: copy rows out, close the connection promptly ──
    public static List<Product> fetchProducts(Connection conn, String category) throws SQLException {
        String sql = "SELECT id, name, price FROM products WHERE category = ?";
        List<Product> results = new ArrayList<>();

        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, category);
            try (ResultSet rs = ps.executeQuery()) {
                while (rs.next()) {
                    results.add(new Product(
                            rs.getLong("id"),
                            rs.getString("name"),
                            rs.getBigDecimal("price")));
                }
            }
        }
        // Connection can be closed here (e.g. by the caller's try-with-resources) —
        // 'results' no longer depends on it at all, same disconnected benefit as
        // CachedRowSet, but using a simple, type-safe, purpose-built Java object.
        return results;
    }

    public static void main(String[] args) throws Exception {
        try (Connection conn = DriverManager.getConnection(
                "jdbc:postgresql://localhost:5432/storedb", "app_user", "secret")) {
            List<Product> products = fetchProducts(conn, "electronics");
            for (Product p : products) {
                System.out.println(p.name() + ": $" + p.price());
            }
        }
    }
}

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.