# 从结果集中检索和修改值

> 原文： [https://docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html](https://docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html)

以下方法， `[CoffeesTable.viewTable](gettingstarted.html)`输出`COFFEES`表的内容，并演示`ResultSet`对象和游标的使用：

```
public static void viewTable(Connection con, String dbName)
    throws SQLException {

    Statement stmt = null;
    String query =
        "select COF_NAME, SUP_ID, PRICE, " +
        "SALES, TOTAL " +
        "from " + dbName + ".COFFEES";

    try {
        stmt = con.createStatement();
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String coffeeName = rs.getString("COF_NAME");
            int supplierID = rs.getInt("SUP_ID");
            float price = rs.getFloat("PRICE");
            int sales = rs.getInt("SALES");
            int total = rs.getInt("TOTAL");
            System.out.println(coffeeName + "\t" + supplierID +
                               "\t" + price + "\t" + sales +
                               "\t" + total);
        }
    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (stmt != null) { stmt.close(); }
    }
}

```

`ResultSet`对象是表示数据库结果集的数据表，通常通过执行查询数据库的语句来生成。例如， `[CoffeeTables.viewTable](gettingstarted.html)`方法在通过`Statement`对象`stmt`执行查询时创建`ResultSet`，`rs`。请注意，可以通过实现`Statement`接口的任何对象创建`ResultSet`对象，包括`PreparedStatement`，`CallableStatement`和`RowSet`。

您可以通过游标访问`ResultSet`对象中的数据。请注意，此游标不是数据库游标。该光标是指向`ResultSet`中一行数据的指针。最初，光标位于第一行之前。方法`ResultSet.next`将光标移动到下一行。如果光标位于最后一行之后，则此方法返回`false`。该方法使用`while`循环重复调用`ResultSet.next`方法，以迭代`ResultSet`中的所有数据。

此页面包含以下主题：

*   [ResultSet 接口](#rs_interface)
*   [从行检索列值](#retrieve_rs)
*   [游标](#cursors)
*   [更新 ResultSet 对象中的行](#rs_update)
*   [使用语句对象进行批量更新](#batch_updates)
*   [在 ResultSet 对象中插入行](#rs_insert)

`ResultSet`接口提供了检索和操作已执行查询结果的方法，`ResultSet`对象可以具有不同的功能和特性。这些特性是类型，并发性和游标 _ 可保持性 _。

### ResultSet 类型

`ResultSet`对象的类型在两个区域中确定其功能级别：可以操作游标的方式，以及`ResultSet`对象如何反映对基础数据源的并发更改。

`ResultSet`对象的灵敏度由三种不同的`ResultSet`类型中的一种决定：

*   `TYPE_FORWARD_ONLY`：无法滚动结果集;它的光标仅向前移动，从第一行之前到最后一行之后。结果集中包含的行取决于底层数据库如何生成结果。也就是说，它包含在执行查询时或检索行时满足查询的行。
*   `TYPE_SCROLL_INSENSITIVE`：结果可以滚动;它的光标可以相对于当前位置向前和向后移动，并且它可以移动到绝对位置。结果集对基础数据源打开时所做的更改不敏感。它包含在执行查询时或检索行时满足查询的行。
*   `TYPE_SCROLL_SENSITIVE`：结果可以滚动;它的光标可以相对于当前位置向前和向后移动，并且它可以移动到绝对位置。结果集反映了在结果集保持打开状态时对基础数据源所做的更改。

默认`ResultSet`类型为`TYPE_FORWARD_ONLY`。

**注意**：并非所有数据库和 JDBC 驱动程序都支持所有`ResultSet`类型。如果支持指定的`ResultSet`类型，则`DatabaseMetaData.supportsResultSetType`方法返回`true`，否则返回`false`。

### ResultSet 并发

`ResultSet`对象的并发性决定了支持的更新功能级别。

有两个并发级别：

*   `CONCUR_READ_ONLY`：无法使用`ResultSet`界面更新`ResultSet`对象。
*   `CONCUR_UPDATABLE`：可以使用`ResultSet`界面更新`ResultSet`对象。

默认`ResultSet`并发是`CONCUR_READ_ONLY`。

**注意**：并非所有 JDBC 驱动程序和数据库都支持并发。如果驱动程序支持指定的并发级别，则`DatabaseMetaData.supportsResultSetConcurrency`返回`true`，否则返回`false`。

方法 `[CoffeesTable.modifyPrices](gettingstarted.html)`演示了如何使用并发级别为`CONCUR_UPDATABLE`的`ResultSet`对象。

### 光标可保持性

调用方法`Connection.commit`可以关闭在当前事务期间创建的`ResultSet`对象。但是，在某些情况下，这可能不是理想的行为。 `ResultSet`属性 _ 可保持性 _ 使应用程序可以控制在调用 commit 时是否关闭`ResultSet`对象（游标）。

以下`ResultSet`常数可以提供给`Connection`方法`createStatement`，`prepareStatement`和`prepareCall`：

*   `HOLD_CURSORS_OVER_COMMIT`：`ResultSet`光标未关闭;它们是 _ 可保持 _：当调用方法`commit`时它们保持打开状态。如果您的应用程序主要使用只读`ResultSet`对象，则可保持游标可能是理想的。
*   `CLOSE_CURSORS_AT_COMMIT`：调用`commit`方法时，`ResultSet`对象（光标）关闭。调用此方法时关闭游标可以为某些应用程序带来更好的性能。

默认光标可保持性因 DBMS 而异。

**注意**：并非所有 JDBC 驱动程序和数据库都支持可保持和不可保留的游标。以下方法`JDBCTutorialUtilities.cursorHoldabilitySupport`输出`ResultSet`对象的默认光标可保持性以及是否支持`HOLD_CURSORS_OVER_COMMIT`和`CLOSE_CURSORS_AT_COMMIT`：

```
public static void cursorHoldabilitySupport(Connection conn)
    throws SQLException {

    DatabaseMetaData dbMetaData = conn.getMetaData();
    System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " +
        ResultSet.HOLD_CURSORS_OVER_COMMIT);

    System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
        ResultSet.CLOSE_CURSORS_AT_COMMIT);

    System.out.println("Default cursor holdability: " +
        dbMetaData.getResultSetHoldability());

    System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " +
        dbMetaData.supportsResultSetHoldability(
            ResultSet.HOLD_CURSORS_OVER_COMMIT));

    System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " +
        dbMetaData.supportsResultSetHoldability(
            ResultSet.CLOSE_CURSORS_AT_COMMIT));
}

```

`ResultSet`接口声明 getter 方法（例如，`getBoolean`和`getLong`），用于从当前行检索列值。您可以使用列的索引号或列的别名或名称来检索值。列索引通常更有效。列从 1 开始编号。为了获得最大的可移植性，每行中的结果集列应按从左到右的顺序读取，每列应只读一次。

例如，以下方法 `[CoffeesTable.alternateViewTable](gettingstarted.html)`，按编号检索列值：

```
public static void alternateViewTable(Connection con)
    throws SQLException {

    Statement stmt = null;
    String query =
        "select COF_NAME, SUP_ID, PRICE, " +
        "SALES, TOTAL from COFFEES";

    try {
        stmt = con.createStatement();
        ResultSet rs = stmt.executeQuery(query);
        while (rs.next()) {
            String coffeeName = rs.getString(1);
            int supplierID = rs.getInt(2);
            float price = rs.getFloat(3);
            int sales = rs.getInt(4);
            int total = rs.getInt(5);
            System.out.println(coffeeName + "\t" + supplierID +
                               "\t" + price + "\t" + sales +
                               "\t" + total);
        }
    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (stmt != null) { stmt.close(); }
    }
}

```

用作 getter 方法输入的字符串不区分大小写。当使用字符串调用 getter 方法并且多个列具有与字符串相同的别名或名称时，将返回第一个匹配列的值。使用字符串而不是整数的选项设计用于在生成结果集的 SQL 查询中使用列别名和名称时使用。对于在查询中明确命名的 _ 而不是 _ 的列（例如，`select * from COFFEES`），最好使用列号。如果使用列名，开发人员应保证使用列别名唯一引用预期的列。列别名有效地重命名结果集的列。要指定列别名，请使用`SELECT`语句中的 SQL `AS`子句。

适当类型的 getter 方法检索每列中的值。例如，在方法 `[CoffeeTables.viewTable](gettingstarted.html)`中，`ResultSet` `rs`的每一行中的第一列是`COF_NAME`，它存储 SQL 类型的值`VARCHAR` ]。检索 SQL 类型`VARCHAR`的值的方法是`getString`。每行中的第二列存储 SQL 类型`INTEGER`的值，并且用于检索该类型的值的方法是`getInt`。

请注意，虽然建议使用方法`getString`来检索 SQL 类型`CHAR`和`VARCHAR`，但可以使用它检索任何基本 SQL 类型。使用`getString`获取所有值非常有用，但它也有其局限性。例如，如果它用于检索数字类型，`getString`会将数值转换为 Java `String`对象，并且必须先将值转换回数字类型，然后才能将其作为数字进行操作。如果将值视为字符串，则没有任何缺点。此外，如果希望应用程序检索除 SQL3 类型之外的任何标准 SQL 类型的值，请使用`getString`方法。

如前所述，您可以通过光标访问`ResultSet`对象中的数据，该光标指向`ResultSet`对象中的一行。但是，首次创建`ResultSet`对象时，光标位于第一行之前。方法 `[CoffeeTables.viewTable](gettingstarted.html)`通过调用`ResultSet.next`方法移动光标。还有其他可用于移动光标的方法：

*   `next`：将光标向前移动一行。如果光标现在位于行上，则返回`true`;如果光标位于最后一行之后，则返回`false`。
*   `previous`：向后移动光标一行。如果光标现在位于行上，则返回`true`;如果光标位于第一行之前，则返回`false`。
*   `first`：将光标移动到`ResultSet`对象的第一行。如果光标现在位于第一行，则返回`true`;如果`ResultSet`对象不包含任何行，则返回`false`。
*   `last:`：将光标移动到`ResultSet`对象的最后一行。如果光标现在位于最后一行，则返回`true`;如果`ResultSet`对象不包含任何行，则返回`false`。
*   `beforeFirst`：将光标定位在`ResultSet`对象的开头，在第一行之前。如果`ResultSet`对象不包含任何行，则此方法无效。
*   `afterLast`：将光标定位在最后一行之后的`ResultSet`对象的末尾。如果`ResultSet`对象不包含任何行，则此方法无效。
*   `relative(int rows)`：相对于当前位置移动光标。
*   `absolute(int row)`：将光标定位在参数`row`指定的行上。

请注意，`ResultSet`的默认灵敏度为`TYPE_FORWARD_ONLY`，这意味着它无法滚动;如果无法滚动`ResultSet`，则无法调用任何移动光标的方法，除了`next`。方法 `[CoffeesTable.modifyPrices](gettingstarted.html)`，如下节所述，演示了如何移动`ResultSet`的光标。

您无法更新默认`ResultSet`对象，只能向前移动光标。但是，您可以创建可以滚动的`ResultSet`对象（光标可以向后移动或移动到绝对位置）并更新。

以下方法， `[CoffeesTable.modifyPrices](gettingstarted.html)`，将每行的`PRICE`列乘以参数`percentage`：

```
public void modifyPrices(float percentage) throws SQLException {

    Statement stmt = null;
    try {
        stmt = con.createStatement();
        stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
                   ResultSet.CONCUR_UPDATABLE);
        ResultSet uprs = stmt.executeQuery(
            "SELECT * FROM " + dbName + ".COFFEES");

        while (uprs.next()) {
            float f = uprs.getFloat("PRICE");
            uprs.updateFloat( "PRICE", f * percentage);
            uprs.updateRow();
        }

    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (stmt != null) { stmt.close(); }
    }
}

```

字段`ResultSet.TYPE_SCROLL_SENSITIVE`创建一个`ResultSet`对象，其光标可以相对于当前位置向前和向后移动并移动到绝对位置。字段`ResultSet.CONCUR_UPDATABLE`创建可以更新的`ResultSet`对象。有关您可以指定的其他字段，请参阅`ResultSet` Javadoc 以修改`ResultSet`对象的行为。

方法`ResultSet.updateFloat`更新指定的列（在此示例中，`PRICE`具有光标所在行中指定的`float`值。`ResultSet`包含各种更新程序方法，使您可以更新各种数据的列值但是，这些更新程序方法都不会修改数据库;您必须调用方法`ResultSet.updateRow`来更新数据库。

`Statement`，`PreparedStatement`和`CallableStatement`对象具有与之关联的命令列表。该列表可能包含更新，插入或删除行的语句;它也可能包含`CREATE TABLE`和`DROP TABLE`等 DDL 语句。但是，它不能包含会产生`ResultSet`对象的语句，例如`SELECT`语句。换句话说，列表只能包含产生更新计数的语句。

该列表在创建时与`Statement`对象关联，最初为空。您可以使用方法`addBatch`将 SQL 命令添加到此列表中，并使用方法`clearBatch`将其清空。完成向列表添加语句后，调用方法`executeBatch`将它们全部发送到数据库以作为一个单元或批处理执行。

例如，以下方法 `[CoffeesTable.batchUpdate](gettingstarted.html)`通过批量更新向`COFFEES`表添加四行：

```
public void batchUpdate() throws SQLException {

    Statement stmt = null;
    try {
        this.con.setAutoCommit(false);
        stmt = this.con.createStatement();

        stmt.addBatch(
            "INSERT INTO COFFEES " +
            "VALUES('Amaretto', 49, 9.99, 0, 0)");

        stmt.addBatch(
            "INSERT INTO COFFEES " +
            "VALUES('Hazelnut', 49, 9.99, 0, 0)");

        stmt.addBatch(
            "INSERT INTO COFFEES " +
            "VALUES('Amaretto_decaf', 49, " +
            "10.99, 0, 0)");

        stmt.addBatch(
            "INSERT INTO COFFEES " +
            "VALUES('Hazelnut_decaf', 49, " +
            "10.99, 0, 0)");

        int [] updateCounts = stmt.executeBatch();
        this.con.commit();

    } catch(BatchUpdateException b) {
        JDBCTutorialUtilities.printBatchUpdateException(b);
    } catch(SQLException ex) {
        JDBCTutorialUtilities.printSQLException(ex);
    } finally {
        if (stmt != null) { stmt.close(); }
        this.con.setAutoCommit(true);
    }
}

```

以下行禁用`Connection`对象 con 的自动提交模式，以便在调用方法`executeBatch`时不会自动提交或回滚事务。

```
this.con.setAutoCommit(false);

```

要允许正确的错误处理，应始终在开始批量更新之前禁用自动提交模式。

方法`Statement.addBatch`将命令添加到与`Statement`对象`stmt`关联的命令列表中。在此示例中，这些命令都是`INSERT INTO`语句，每个语句都添加一行，包含五个列值。列`COF_NAME`和`PRICE`的值分别是咖啡的名称及其价格。每行中的第二个值为 49，因为这是供应商 Superior Coffee 的标识号。最后两个值，列`SALES`和`TOTAL`的条目都开始为零，因为还没有销售。 （`SALES`是本周销售的该行咖啡的磅数; `TOTAL`是该咖啡累计销售量的总和。）

以下行将添加到其命令列表中的四个 SQL 命令发送到要作为批处理执行的数据库：

```
int [] updateCounts = stmt.executeBatch();

```

请注意，`stmt`使用方法`executeBatch`发送一批插入，而不是方法`executeUpdate`，它只发送一个命令并返回单个更新计数。 DBMS 按照将命令添加到命令列表的顺序执行命令，因此它首先添加 Amaretto 的值行，然后添加 Hazelnut，然后是 Amaretto decaf，最后是 Hazelnut decaf。如果所有四个命令都成功执行，则 DBMS 将按照执行顺序为每个命令返回更新计数。更新计数表示每个命令影响的行数存储在数组`updateCounts`中。

如果批处理中的所有四个命令都成功执行，`updateCounts`将包含四个值，所有这些值都是 1，因为插入会影响一行。与`stmt`关联的命令列表现在将为空，因为当`stmt`调用方法`executeBatch`时，先前添加的四个命令被发送到数据库。您可以随时使用方法`clearBatch`显式清空此命令列表。

`Connection.commit`方法使`COFFEES`表的批量更新成为永久性。需要显式调用此方法，因为此连接的自动提交模式先前已禁用。

以下行为当前`Connection`对象启用自动提交模式。

```
this.con.setAutoCommit(true);

```

现在，示例中的每个语句在执行后都会自动提交，不再需要调用方法`commit`。

### 执行参数化批量更新

也可以进行参数化批量更新，如下面的代码片段所示，其中`con`是`Connection`对象：

```
con.setAutoCommit(false);
PreparedStatement pstmt = con.prepareStatement(
                              "INSERT INTO COFFEES VALUES( " +
                              "?, ?, ?, ?, ?)");
pstmt.setString(1, "Amaretto");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

pstmt.setString(1, "Hazelnut");
pstmt.setInt(2, 49);
pstmt.setFloat(3, 9.99);
pstmt.setInt(4, 0);
pstmt.setInt(5, 0);
pstmt.addBatch();

// ... and so on for each new
// type of coffee

int [] updateCounts = pstmt.executeBatch();
con.commit();
con.setAutoCommit(true);

```

### 处理批量更新例外

如果（1）您添加到批处理中的一个 SQL 语句生成结果集（通常是查询）或（2）批处理中的一个 SQL 语句，则在调用方法`executeBatch`时将获得`BatchUpdateException`由于某些其他原因未成功执行。

您不应该向一批 SQL 命令添加查询（`SELECT`语句），因为返回更新计数数组的方法`executeBatch`需要每个成功执行的 SQL 语句的更新计数。这意味着只有返回更新计数的命令（诸如`INSERT INTO`，`UPDATE`，`DELETE`之类的命令）或返回 0 的命令（例如`CREATE TABLE`，`DROP TABLE`，`ALTER TABLE`）才能成功执行使用`executeBatch`方法的批次。

`BatchUpdateException`包含一个更新计数数组，类似于方法`executeBatch`返回的数组。在这两种情况下，更新计数的顺序与生成它们的命令的顺序相同。这告诉您批处理中有多少命令成功执行以及它们是哪些命令。例如，如果成功执行了五个命令，则该数组将包含五个数字：第一个是第一个命令的更新计数，第二个是第二个命令的更新计数，依此类推。

`BatchUpdateException`源自`SQLException`。这意味着您可以使用`SQLException`对象可用的所有方法。以下方法， `[JDBCTutorialUtilities.printBatchUpdateException](gettingstarted.html)`打印所有`SQLException`信息以及`BatchUpdateException`对象中包含的更新计数。因为`BatchUpdateException.getUpdateCounts`返回`int`数组，代码使用`for`循环打印每个更新计数：

```
public static void printBatchUpdateException(BatchUpdateException b) {

    System.err.println("----BatchUpdateException----");
    System.err.println("SQLState:  " + b.getSQLState());
    System.err.println("Message:  " + b.getMessage());
    System.err.println("Vendor:  " + b.getErrorCode());
    System.err.print("Update counts:  ");
    int [] updateCounts = b.getUpdateCounts();

    for (int i = 0; i < updateCounts.length; i++) {
        System.err.print(updateCounts[i] + "   ");
    }
}

```

**注意**：并非所有 JDBC 驱动程序都支持使用`ResultSet`接口插入新行。如果尝试插入新行并且 JDBC 驱动程序数据库不支持此功能，则会引发`SQLFeatureNotSupportedException`异常。

以下方法 `[CoffeesTable.insertRow](gettingstarted.html)`通过`ResultSet`对象在`COFFEES`中插入一行：

```
public void insertRow(String coffeeName, int supplierID,
                      float price, int sales, int total)
    throws SQLException {

    Statement stmt = null;
    try {
        stmt = con.createStatement(
            ResultSet.TYPE_SCROLL_SENSITIVE
            ResultSet.CONCUR_UPDATABLE);

        ResultSet uprs = stmt.executeQuery(
            "SELECT * FROM " + dbName +
            ".COFFEES");

        uprs.moveToInsertRow();
        uprs.updateString("COF_NAME", coffeeName);
        uprs.updateInt("SUP_ID", supplierID);
        uprs.updateFloat("PRICE", price);
        uprs.updateInt("SALES", sales);
        uprs.updateInt("TOTAL", total);

        uprs.insertRow();
        uprs.beforeFirst();
    } catch (SQLException e ) {
        JDBCTutorialUtilities.printSQLException(e);
    } finally {
        if (stmt != null) { stmt.close(); }
    }
}

```

此示例使用两个参数`ResultSet.TYPE_SCROLL_SENSITIVE`和`ResultSet.CONCUR_UPDATABLE`调用`Connection.createStatement`方法。第一个值使`ResultSet`对象的光标可以向前和向后移动。如果要将行插入`ResultSet`对象，则需要第二个值`ResultSet.CONCUR_UPDATABLE`;它指定它可以更新。

在 getter 方法中使用字符串的相同规定也适用于 updater 方法。

方法`ResultSet.moveToInsertRow`将光标移动到插入行。插入行是与可更新结果集关联的特殊行。它本质上是一个缓冲区，可以通过在将行插入结果集之前调用 updater 方法来构造新行。例如，此方法调用方法`ResultSet.updateString`将插入行的`COF_NAME`列更新为`Kona`。

方法`ResultSet.insertRow`将插入行的内容插入`ResultSet`对象并插入数据库。

**注**：使用`ResultSet.insertRow`插入行后，应将光标移动到插入行以外的行。例如，此示例使用方法`ResultSet.beforeFirst`将其移动到结果集中的第一行之前。如果应用程序的另一部分使用相同的结果集并且光标仍指向插入行，则可能会出现意外结果。