文档

Java™教程
隐藏目录
从结果集中检索和修改值
路径: JDBC数据库访问
课程: JDBC基础知识

从结果集中检索和修改值

下面的方法CoffeesTable.viewTable输出COFFEES表的内容,并演示了ResultSet对象和游标的使用:

  public static void viewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement 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 + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

ResultSet对象是表示数据库结果集的数据表,通常通过执行查询数据库的语句生成。例如,当执行CoffeeTables.viewTable方法时,会通过Statement对象stmt执行查询,并创建一个ResultSet对象rs。注意,ResultSet对象可以通过实现Statement接口的任何对象创建,包括PreparedStatementCallableStatementRowSet

通过游标访问ResultSet对象中的数据。注意,这里的游标不是数据库游标,而是指向ResultSet中一行数据的指针。初始时,游标位于第一行之前。方法ResultSet.next将游标移动到下一行。如果游标位于最后一行之后,该方法返回false。该方法使用while循环反复调用ResultSet.next方法,以迭代遍历ResultSet中的所有数据。

本页面涵盖以下主题:

ResultSet接口

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

结果集类型

ResultSet对象的类型决定了它在两个方面的功能级别:光标可以如何操作以及底层数据源的并发更改如何反映在ResultSet对象中。

ResultSet对象的灵敏度由三种不同的ResultSet类型之一确定:

默认的ResultSet类型是TYPE_FORWARD_ONLY

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

结果集并发性

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

有两个并发级别:

默认的ResultSet并发性是CONCUR_READ_ONLY

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

方法CoffeesTable.modifyPrices演示了如何使用并发级别为CONCUR_UPDATABLEResultSet对象。

游标保持性

调用Connection.commit方法可以关闭在当前事务中创建的ResultSet对象。然而,在某些情况下,这可能不是期望的行为。ResultSet属性holdability可以使应用程序控制在调用commit时是否关闭ResultSet对象(游标)。

以下ResultSet常量可供Connection方法createStatementprepareStatementprepareCall使用:

默认的游标保持性取决于您的DBMS。

注意:并非所有的JDBC驱动程序和数据库都支持可保持和不可保持的游标。以下方法JDBCTutorialUtilities.cursorHoldabilitySupport输出ResultSet对象的默认游标保持性以及是否支持HOLD_CURSORS_OVER_COMMITCLOSE_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方法(例如getBooleangetLong)。可以使用列的索引号或别名或名称来检索值。列索引通常更有效率。列从1开始编号。为了最大的可移植性,应该按从左到右的顺序读取每行中的结果集列,并且每列只能读取一次。

例如,下面的方法CoffeesTable.alternateViewTable通过编号检索列值:

  public static void alternateViewTable(Connection con) throws SQLException {
    String query = "select COF_NAME, SUP_ID, PRICE, SALES, TOTAL from COFFEES";
    try (Statement 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 + ", " + supplierID + ", " + price +
                           ", " + sales + ", " + total);
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

作为getter方法的输入的字符串是不区分大小写的。当使用一个字符串调用getter方法时,如果有多个列具有相同的别名或名称与该字符串相匹配,则返回第一个匹配列的值。使用字符串而不是整数的选项是为了在生成结果集的SQL查询中使用列别名和名称。对于在查询中没有显式命名的列(例如select * from COFFEES),最好使用列号。如果使用列名,则开发人员应该使用列别名来确保它们唯一地指向所需的列。列别名有效地重命名了结果集的列。要指定列别名,请在SELECT语句中使用SQL AS子句。

适当类型的getter方法检索每列的值。例如,在方法CoffeeTables.viewTable中,ResultSet rs的每一行的第一列是COF_NAME,它存储了一个SQL类型VARCHAR的值。检索SQL类型VARCHAR的值的方法是getString。每一行的第二列存储了一个SQL类型INTEGER的值,检索该类型值的方法是getInt

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

游标

如前所述,您可以通过游标访问ResultSet对象中的数据,游标指向ResultSet对象中的一行。但是,当ResultSet对象首次创建时,游标位于第一行之前。方法CoffeeTables.viewTable通过调用ResultSet.next方法移动游标。还有其他可用于移动游标的方法:

请注意,默认的ResultSet的灵敏度为TYPE_FORWARD_ONLY,这意味着它不能滚动;如果您的ResultSet不能滚动,则不能调用移动游标的这些方法之一,除了next。以下部分将介绍方法CoffeesTable.modifyPrices,演示如何移动ResultSet的游标。

更新ResultSet对象中的行

您无法更新默认的ResultSet对象,并且只能向前移动其游标。但是,您可以创建可滚动(游标可以向后移动或移动到绝对位置)和可更新的ResultSet对象。

以下方法CoffeesTable.modifyPrices,将每行的PRICE列乘以参数percentage

  public void modifyPrices(float percentage) throws SQLException {
    try (Statement stmt =
      con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE)) {
      ResultSet uprs = stmt.executeQuery("SELECT * FROM COFFEES");
      while (uprs.next()) {
        float f = uprs.getFloat("PRICE");
        uprs.updateFloat("PRICE", f * percentage);
        uprs.updateRow();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

字段ResultSet.TYPE_SCROLL_SENSITIVE创建一个ResultSet对象,它的游标可以相对于当前位置向前和向后移动到绝对位置。字段ResultSet.CONCUR_UPDATABLE创建一个可以更新的ResultSet对象。请参阅ResultSet Javadoc以了解可以指定的其他字段,以修改ResultSet对象的行为。

方法ResultSet.updateFloat更新光标所在行的指定列(在本例中为PRICE)为指定的float值。ResultSet包含各种更新器方法,使您能够更新各种数据类型的列值。但是,这些更新器方法都不会修改数据库;您必须调用方法ResultSet.updateRow来更新数据库。

使用Statement对象进行批量更新

StatementPreparedStatementCallableStatement对象有一个与之关联的命令列表。该列表可以包含用于更新、插入或删除行的语句,也可以包含DDL语句,例如CREATE TABLEDROP TABLE。但是,它不能包含会生成ResultSet对象的语句,如SELECT语句。换句话说,该列表只能包含生成更新计数的语句。

Statement对象关联的列表在创建时为空。您可以使用方法addBatch将SQL命令添加到该列表中,并使用方法clearBatch将其清空。当您添加完所有语句到列表后,调用方法executeBatch将它们作为一个单元或批量发送到数据库执行。

例如,以下方法CoffeesTable.batchUpdate通过批量更新向COFFEES表中添加了四行:

  public void batchUpdate() throws SQLException {
    con.setAutoCommit(false);
    try (Statement stmt = 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();
      con.commit();
    } catch (BatchUpdateException b) {
      JDBCTutorialUtilities.printBatchUpdateException(b);
    } catch (SQLException ex) {
      JDBCTutorialUtilities.printSQLException(ex);
    } finally {
      con.setAutoCommit(true);
    }
  }

以下代码行禁用了Connection对象con的自动提交模式,这样当调用executeBatch方法时,事务将不会自动提交或回滚。

con.setAutoCommit(false);

为了正确处理错误,您应该在开始批量更新之前始终禁用自动提交模式。

方法Statement.addBatch将一个命令添加到与Statement对象stmt关联的命令列表中。在这个示例中,这些命令都是INSERT INTO语句,每个语句都添加了一个由五个列值组成的行。列COF_NAMEPRICE的值分别为咖啡的名称和价格。每行的第二个值是49,因为这是供应商Superior Coffee的标识号。最后两个值,即列SALESTOTAL的条目,初始值都为零,因为目前还没有销售。(SALES是本行咖啡在当前周销售的磅数;TOTAL是本咖啡所有累积销售额的总和。)

以下代码行将已添加到其命令列表的四个SQL命令发送到数据库以作为批处理执行:

int[] updateCounts = stmt.executeBatch();

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

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

Connection.commit方法将对COFFEES表的更新批量操作变为永久性。之前禁用了此连接的自动提交模式,因此需要显式调用此方法。

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

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();

// ... 根据每种新咖啡进行类似的操作

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

处理批量更新异常

如果您在调用executeBatch方法时,批处理中的某个SQL语句(通常是查询)产生了结果集,或者批处理中的某个SQL语句由于其他原因未能成功执行,则会抛出BatchUpdateException异常。

您不应将查询(SELECT语句)添加到SQL命令的批处理中,因为executeBatch方法会返回一个更新计数的数组,该数组期望每个成功执行的SQL语句都返回一个更新计数。这意味着只有返回更新计数的命令(如INSERT INTOUPDATEDELETE)或返回0的命令(如CREATE TABLEDROP TABLEALTER TABLE)可以成功地与executeBatch方法一起批量执行。

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

BatchUpdateException派生自SQLException。这意味着您可以使用所有适用于SQLException对象的方法。下面的方法JDBCTutorialUtilities.printBatchUpdateException打印了所有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] + "   ");
    }
  }

在 ResultSet 对象中插入行

注意:并非所有的 JDBC 驱动程序都支持使用 ResultSet 接口插入新行。如果您尝试插入新行,而您的 JDBC 驱动程序数据库不支持此功能,则会抛出 SQLFeatureNotSupportedException 异常。

下面的方法 CoffeesTable.insertRow 通过 ResultSet 对象将一行插入到 COFFEES 表中:

  public void insertRow(String coffeeName, int supplierID, float price,
                        int sales, int total) throws SQLException {
    
    try (Statement stmt =
          con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE))
    {      
      ResultSet uprs = stmt.executeQuery("SELECT * FROM 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);
    }
  }

此示例调用 Connection.createStatement 方法,并传入两个参数,ResultSet.TYPE_SCROLL_SENSITIVEResultSet.CONCUR_UPDATABLE。第一个值使得 ResultSet 对象的游标可以前后移动。第二个值 ResultSet.CONCUR_UPDATABLE 是插入行到 ResultSet 对象所需的;它指定结果集可以被更新。

使用字符串的 getter 方法的限制同样适用于 updater 方法。

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

ResultSet.insertRow 方法将插入行的内容插入到 ResultSet 对象和数据库中。

注意:使用 ResultSet.insertRow 插入行后,应将游标移动到插入行以外的行。例如,此示例使用 ResultSet.beforeFirst 方法将游标移动到结果集中的第一行之前。如果应用程序的其他部分使用相同的结果集并且游标仍指向插入行,则可能会导致意外的结果。


上一页:设置表
下一页:使用预编译语句