724.md 21.7 KB
Newer Older
W
init  
wizardforcel 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
# 从结果集中检索和修改值

> 原文: [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`对象的并发性决定了支持的更新功能级别。

有两个并发级别:

W
wizardforcel 已提交
74 75
*   `CONCUR_READ_ONLY`:无法使用`ResultSet`接口更新`ResultSet`对象。
*   `CONCUR_UPDATABLE`:可以使用`ResultSet`接口更新`ResultSet`对象。
W
init  
wizardforcel 已提交
76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393

默认`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`将其移动到结果集中的第一行之前。如果应用程序的另一部分使用相同的结果集并且光标仍指向插入行,则可能会出现意外结果。