从结果集中检索和修改值

以下方法CoffeesTable.viewTable输出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方法在通过Statement对象stmt执行查询时会创建ResultSetrs。请注意,可以通过实现Statementinterface的任何对象(包括PreparedStatementCallableStatementRowSet)来创建ResultSet对象。

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

此页面包含以下主题:

ResultSet Interface

ResultSetinterface提供了检索和处理已执行查询结果的方法,而ResultSet对象可以具有不同的功能和特性。这些 Feature 是类型,并发性和游标* holdability *。

ResultSet Types

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 Concurrency

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

有两个并发级别:

  • CONCUR_READ_ONLY:无法使用ResultSetinterface更新ResultSet对象。

  • CONCUR_UPDATABLE:可以使用ResultSetinterface更新ResultSet对象。

默认的ResultSet并发是CONCUR_READ_ONLY

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

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

Cursor Holdability

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

以下ResultSet常量可以提供给Connection方法createStatementprepareStatementprepareCall

  • HOLD_CURSORS_OVER_COMMITResultSet游标未关闭;它们是* holdable *的:它们在调用方法commit时保持打开状态。如果您的应用程序主要使用只读的ResultSet对象,则可保持游标可能是理想的选择。

  • CLOSE_CURSORS_AT_COMMITResultSet对象(光标)在调用commit方法时关闭。调用此方法时关闭游标可以提高某些应用程序的性能。

默认的游标可保留性取决于您的 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));
}

从行中检索列值

ResultSetinterface声明用于从当前行中检索列值的 getter 方法(例如getBooleangetLong)。您可以使用列的索引号或列的别名或名称来检索值。列索引通常更有效。列从 1 开始编号。为实现最大的可移植性,应按从左到右的 Sequences 读取每一行中的结果集列,并且每一列只能读取一次。

例如,以下方法CoffeesTable.alternateViewTable按数字检索列值:

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 方法 Importing 的字符串 不区分大小写。当使用字符串 调用 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方法。

Cursors

如前所述,您可以通过游标访问ResultSet对象中的数据,该游标指向ResultSet对象中的一行。但是,首次创建ResultSet对象时,光标位于第一行之前。方法CoffeeTables.viewTable通过调用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演示了如何移动ResultSet的光标。

更新 ResultSet 对象中的行

您无法更新默认的ResultSet对象,只能将其光标向前移动。但是,您可以创建ResultSet个可以滚动的对象(光标可以向后移动或移至绝对位置)并进行更新。

以下方法CoffeesTable.modifyPrices将每行的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对象行为的其他字段,请参见ResultSet Javadoc。

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

使用语句对象进行批处理更新

StatementPreparedStatementCallableStatement对象具有与其关联的命令列表。该列表可能包含用于更新,插入或删除行的语句;并且还可能包含 DDL 语句,例如CREATE TABLEDROP TABLE。但是,它不能包含将产生ResultSet对象的语句,例如SELECT语句。换句话说,列表只能包含产生更新计数的语句。

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

例如,以下方法CoffeesTable.batchUpdate使用批处理更新将四行添加到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 禁用了自动提交 Pattern,以便在调用方法executeBatch时不会自动提交或回滚事务。

this.con.setAutoCommit(false);

为了进行正确的错误处理,应始终在开始批量更新之前始终禁用自动提交 Pattern。

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

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

int [] updateCounts = stmt.executeBatch();

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

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

Connection.commit方法使对COFFEES表的更新批量永久化。由于以前已禁用此连接的自动提交 Pattern,因此需要显式调用此方法。

下一行为当前的Connection对象启用自动提交 Pattern。

this.con.setAutoCommit(true);

现在示例中的每个语句在执行后将自动提交,并且不再需要调用方法commit

执行参数化的批次更新

也可以进行参数化的批处理更新,如以下代码片段所示,其中conConnection对象:

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

处理批处理更新异常

调用方法executeBatch时,如果(1)添加到该批处理中的一条 SQL 语句产生一个结果集(通常是一个查询),或者(2)批处理中的其中一条 SQL 语句未成功执行,您将得到BatchUpdateException由于其他原因。

您不应该在一批 SQL 命令中添加查询(一个SELECT语句),因为返回更新计数数组的executeBatch方法期望成功执行的每个 SQL 语句都有一个更新计数。这意味着只有返回更新计数的命令(如INSERT INTOUPDATEDELETE)或返回 0 的命令(如CREATE TABLEDROP TABLEALTER TABLE)才能使用executeBatch方法成功地成批执行。

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

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 驱动程序都支持使用ResultSetinterface插入新行。如果您try插入新行,并且 JDBC 驱动程序数据库不支持此功能,则会引发SQLFeatureNotSupportedException异常。

以下方法CoffeesTable.insertRow通过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_SENSITIVEResultSet.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将其移动到结果集中的第一行之前。如果您的应用程序的另一部分使用相同的结果集,并且光标仍指向插入行,则可能会出现意外的结果。