1. 概述

Java Database Connectivity (JDBC) API 提供了从 Java 应用访问数据库的能力。只要对应的 JDBC 驱动可用,我们就可以通过 JDBC 连接任何数据库。

ResultSet 是执行数据库查询后返回的数据表形式的结果集。在本教程中,我们将深入探讨 ResultSet API 的使用方式。

2. 如何获取 ResultSet

我们通常通过调用实现了 Statement 接口的对象的 executeQuery() 方法来获取 ResultSetPreparedStatementCallableStatement 都是 Statement 的子接口:

PreparedStatement pstmt = dbConnection.prepareStatement("select * from employees");
ResultSet rs = pstmt.executeQuery();

ResultSet 对象内部维护一个游标(cursor),指向当前结果集中的某一行数据。我们可以通过 next() 方法遍历结果集中的记录。

在遍历过程中,我们使用 getX() 方法来获取各列的值,其中 X 表示列的数据类型。我们也可以将列名作为参数传入:

while(rs.next()) {
    String name = rs.getString("name");
    Integer empId = rs.getInt("emp_id");
    Double salary = rs.getDouble("salary");
    String position = rs.getString("position");
}

此外,我们也可以使用列的索引号来代替列名。索引号从 1 开始,顺序与 SQL 查询中的列顺序一致:

Integer empId = rs.getInt(1);
String name = rs.getString(2);
String position = rs.getString(3);
Double salary = rs.getDouble(4);

3. 获取 ResultSet 元数据

本节我们将介绍如何从 ResultSet 中提取列的元数据信息。

首先,通过 getMetaData() 方法获取 ResultSetMetaData 对象:

ResultSetMetaData metaData = rs.getMetaData();

接着,我们可以获取列的数量:

Integer columnCount = metaData.getColumnCount();

还可以使用以下方法获取各列的详细信息:

getColumnName(int columnNumber) – 获取列名
getColumnLabel(int columnNumber) – 获取查询中定义的列别名(如 AS 后面的内容)
getTableName(int columnNumber) – 获取该列所属的表名
getColumnClassName(int columnNumber) – 获取该列对应的 Java 类型
getColumnTypeName(int columnNumber) – 获取该列在数据库中的类型
getColumnType(int columnNumber) – 获取该列的 SQL 类型代码
isAutoIncrement(int columnNumber) – 判断是否为自增列
isCaseSensitive(int columnNumber) – 判断是否区分大小写
isSearchable(int columnNumber) – 判断是否可用于 WHERE 子句
isCurrency(int columnNumber) – 判断是否为货币类型
isNullable(int columnNumber) – 判断是否可为空(0=不可为空,1=可为空,2=未知)
isSigned(int columnNumber) – 判断是否为有符号类型

示例:遍历所有列并获取其元数据:

for (int columnNumber = 1; columnNumber <= columnCount; columnNumber++) {
    String catalogName = metaData.getCatalogName(columnNumber);
    String className = metaData.getColumnClassName(columnNumber);
    String label = metaData.getColumnLabel(columnNumber);
    String name = metaData.getColumnName(columnNumber);
    String typeName = metaData.getColumnTypeName(columnNumber);
    int type = metaData.getColumnType(columnNumber);
    String tableName = metaData.getTableName(columnNumber);
    String schemaName = metaData.getSchemaName(columnNumber);
    boolean isAutoIncrement = metaData.isAutoIncrement(columnNumber);
    boolean isCaseSensitive = metaData.isCaseSensitive(columnNumber);
    boolean isCurrency = metaData.isCurrency(columnNumber);
    boolean isDefiniteWritable = metaData.isDefinitelyWritable(columnNumber);
    boolean isReadOnly = metaData.isReadOnly(columnNumber);
    boolean isSearchable = metaData.isSearchable(columnNumber);
    boolean isReadable = metaData.isReadOnly(columnNumber);
    boolean isSigned = metaData.isSigned(columnNumber);
    boolean isWritable = metaData.isWritable(columnNumber);
    int nullable = metaData.isNullable(columnNumber);
}

4. 控制 ResultSet 的游标位置

获取 ResultSet 后,游标默认位于第一条记录之前。默认情况下,只能向前遍历,但我们可以使用可滚动的 ResultSet 实现更多导航方式。

4.1. ResultSet 类型

ResultSet 类型决定了游标的移动方式:

TYPE_FORWARD_ONLY – 默认类型,只能从前往后遍历
TYPE_SCROLL_INSENSITIVE – 可前后滚动,不反映底层数据变更
TYPE_SCROLL_SENSITIVE – 可前后滚动,并反映数据变更(数据库支持情况视情况而定)

并不是所有数据库都支持上述所有类型。我们可以通过 DatabaseMetaData 检查是否支持:

DatabaseMetaData dbmd = dbConnection.getMetaData();
boolean isSupported = dbmd.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE);

4.2. 创建可滚动的 ResultSet

创建时需指定类型:

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_INSENSITIVE,
  ResultSet.CONCUR_UPDATABLE); 
ResultSet rs = pstmt.executeQuery();

4.3. 导航方法

next() – 向下移动一行
previous() – 向上移动一行
first() – 移动到第一行
last() – 移动到最后一行
beforeFirst() – 移动到起始位置
afterLast() – 移动到末尾位置
relative(int rows) – 相对当前位置移动指定行数
absolute(int rowNumber) – 移动到指定行号

示例:

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_SENSITIVE,
  ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery();

while (rs.next()) {
    // 正向遍历
}
rs.beforeFirst(); 
rs.afterLast(); 

rs.first(); 
rs.last(); 

rs.absolute(2); 

rs.relative(-1); 
rs.relative(2); 

while (rs.previous()) {
    // 反向遍历
}

4.4. 获取行数

通过 getRow() 获取当前行号:

rs.last();
int rowCount = rs.getRow();

5. 更新 ResultSet 中的数据

默认情况下,ResultSet 是只读的。但我们可以创建可更新的 ResultSet,实现插入、更新、删除操作。

5.1. 并发模式

CONCUR_READ_ONLY – 只读(默认)
CONCUR_UPDATABLE – 可更新

同样地,不是所有数据库都支持所有并发模式。我们可以通过 supportsResultSetConcurrency() 检查支持情况:

DatabaseMetaData dbmd = dbConnection.getMetaData(); 
boolean isSupported = dbmd.supportsResultSetConcurrency(
  ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);

5.2. 获取可更新的 ResultSet

创建时指定并发模式:

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees",
  ResultSet.TYPE_SCROLL_SENSITIVE,
  ResultSet.CONCUR_UPDATABLE);
ResultSet rs = pstmt.executeQuery();

5.3. 更新行数据

使用 updateX() 方法更新指定列的值,然后调用 updateRow() 提交更改:

rs.updateDouble("salary", 1100.0);
rs.updateRow();

也可以使用列索引:

rs.updateDouble(4, 1100.0);
rs.updateRow();

5.4. 插入新行

使用 moveToInsertRow() 进入插入模式,设置各列值后调用 insertRow() 插入:

rs.moveToInsertRow();
rs.updateString("name", "Venkat"); 
rs.updateString("position", "DBA"); 
rs.updateDouble("salary", 925.0);
rs.insertRow();
rs.moveToCurrentRow();

5.5. 删除行

先定位到目标行,再调用 deleteRow()

rs.absolute(2);
rs.deleteRow();

6. 游标的可保持性(Holdability)

控制事务提交后 ResultSet 是否仍然保持打开状态。

6.1. 可保持性类型

CLOSE_CURSORS_AT_COMMIT – 提交后关闭游标
HOLD_CURSORS_OVER_COMMIT – 提交后保持游标打开

不是所有数据库都支持两种类型。可以通过以下方式检查:

boolean isCloseCursorSupported = dbmd.supportsResultSetHoldability(ResultSet.CLOSE_CURSORS_AT_COMMIT);
boolean isOpenCursorSupported = dbmd.supportsResultSetHoldability(ResultSet.HOLD_CURSORS_OVER_COMMIT);
boolean defaultHoldability = dbmd.getResultSetHoldability();

6.2. 创建可保持的 ResultSet

示例:

Statement pstmt = dbConnection.createStatement(
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_UPDATABLE, 
  ResultSet.HOLD_CURSORS_OVER_COMMIT)

在事务提交后继续使用 ResultSet

dbConnection.setAutoCommit(false);
ResultSet rs = pstmt.executeQuery("select * from employees");
while (rs.next()) {
    if(rs.getString("name").equalsIgnoreCase("john")) {
        rs.updateString("name", "John Doe");
        rs.updateRow();
        dbConnection.commit();
    }                
}
rs.last();

⚠️ 注意:MySQL 默认支持 HOLD_CURSORS_OVER_COMMIT,MSSQL 支持 CLOSE_CURSORS_AT_COMMIT

7. 控制获取数据的批次大小(Fetch Size)

用于控制每次从数据库获取多少行数据,避免内存溢出。

7.1. 在 Statement 上设置

PreparedStatement pstmt = dbConnection.prepareStatement(
  "select * from employees", 
  ResultSet.TYPE_SCROLL_SENSITIVE, 
  ResultSet.CONCUR_READ_ONLY);
pstmt.setFetchSize(10);

7.2. 在 ResultSet 上设置

ResultSet rs = pstmt.executeQuery();
rs.setFetchSize(20); 

动态调整示例:

int rowCount = 0;
while (rs.next()) { 
    if (rowCount == 30) {
        rs.setFetchSize(20); 
    }
    rowCount++;
}

8. 总结

本文详细介绍了 ResultSet 的各种使用方式,包括遍历、更新、元数据获取、游标导航、事务保持性及批量加载控制。需要注意的是,很多高级特性依赖于数据库的支持,使用前务必确认兼容性。



原始标题:Guide to the JDBC ResultSet Interface | Baeldung