从结果集中检索和修改值

原文: https://docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html

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

  1. public static void viewTable(Connection con, String dbName)
  2. throws SQLException {
  3. Statement stmt = null;
  4. String query =
  5. "select COF_NAME, SUP_ID, PRICE, " +
  6. "SALES, TOTAL " +
  7. "from " + dbName + ".COFFEES";
  8. try {
  9. stmt = con.createStatement();
  10. ResultSet rs = stmt.executeQuery(query);
  11. while (rs.next()) {
  12. String coffeeName = rs.getString("COF_NAME");
  13. int supplierID = rs.getInt("SUP_ID");
  14. float price = rs.getFloat("PRICE");
  15. int sales = rs.getInt("SALES");
  16. int total = rs.getInt("TOTAL");
  17. System.out.println(coffeeName + "\t" + supplierID +
  18. "\t" + price + "\t" + sales +
  19. "\t" + total);
  20. }
  21. } catch (SQLException e ) {
  22. JDBCTutorialUtilities.printSQLException(e);
  23. } finally {
  24. if (stmt != null) { stmt.close(); }
  25. }
  26. }

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

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

此页面包含以下主题:

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_UPDATABLEResultSet对象。

光标可保持性

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

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

  • HOLD_CURSORS_OVER_COMMITResultSet光标未关闭;它们是可保持:当调用方法commit时它们保持打开状态。如果您的应用程序主要使用只读ResultSet对象,则可保持游标可能是理想的。
  • CLOSE_CURSORS_AT_COMMIT:调用commit方法时,ResultSet对象(光标)关闭。调用此方法时关闭游标可以为某些应用程序带来更好的性能。

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

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

  1. public static void cursorHoldabilitySupport(Connection conn)
  2. throws SQLException {
  3. DatabaseMetaData dbMetaData = conn.getMetaData();
  4. System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " +
  5. ResultSet.HOLD_CURSORS_OVER_COMMIT);
  6. System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
  7. ResultSet.CLOSE_CURSORS_AT_COMMIT);
  8. System.out.println("Default cursor holdability: " +
  9. dbMetaData.getResultSetHoldability());
  10. System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " +
  11. dbMetaData.supportsResultSetHoldability(
  12. ResultSet.HOLD_CURSORS_OVER_COMMIT));
  13. System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " +
  14. dbMetaData.supportsResultSetHoldability(
  15. ResultSet.CLOSE_CURSORS_AT_COMMIT));
  16. }

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

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

  1. public static void alternateViewTable(Connection con)
  2. throws SQLException {
  3. Statement stmt = null;
  4. String query =
  5. "select COF_NAME, SUP_ID, PRICE, " +
  6. "SALES, TOTAL from COFFEES";
  7. try {
  8. stmt = con.createStatement();
  9. ResultSet rs = stmt.executeQuery(query);
  10. while (rs.next()) {
  11. String coffeeName = rs.getString(1);
  12. int supplierID = rs.getInt(2);
  13. float price = rs.getFloat(3);
  14. int sales = rs.getInt(4);
  15. int total = rs.getInt(5);
  16. System.out.println(coffeeName + "\t" + supplierID +
  17. "\t" + price + "\t" + sales +
  18. "\t" + total);
  19. }
  20. } catch (SQLException e ) {
  21. JDBCTutorialUtilities.printSQLException(e);
  22. } finally {
  23. if (stmt != null) { stmt.close(); }
  24. }
  25. }

用作 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 类型CHARVARCHAR,但可以使用它检索任何基本 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

  1. public void modifyPrices(float percentage) throws SQLException {
  2. Statement stmt = null;
  3. try {
  4. stmt = con.createStatement();
  5. stmt = con.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE,
  6. ResultSet.CONCUR_UPDATABLE);
  7. ResultSet uprs = stmt.executeQuery(
  8. "SELECT * FROM " + dbName + ".COFFEES");
  9. while (uprs.next()) {
  10. float f = uprs.getFloat("PRICE");
  11. uprs.updateFloat( "PRICE", f * percentage);
  12. uprs.updateRow();
  13. }
  14. } catch (SQLException e ) {
  15. JDBCTutorialUtilities.printSQLException(e);
  16. } finally {
  17. if (stmt != null) { stmt.close(); }
  18. }
  19. }

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

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

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

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

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

  1. public void batchUpdate() throws SQLException {
  2. Statement stmt = null;
  3. try {
  4. this.con.setAutoCommit(false);
  5. stmt = this.con.createStatement();
  6. stmt.addBatch(
  7. "INSERT INTO COFFEES " +
  8. "VALUES('Amaretto', 49, 9.99, 0, 0)");
  9. stmt.addBatch(
  10. "INSERT INTO COFFEES " +
  11. "VALUES('Hazelnut', 49, 9.99, 0, 0)");
  12. stmt.addBatch(
  13. "INSERT INTO COFFEES " +
  14. "VALUES('Amaretto_decaf', 49, " +
  15. "10.99, 0, 0)");
  16. stmt.addBatch(
  17. "INSERT INTO COFFEES " +
  18. "VALUES('Hazelnut_decaf', 49, " +
  19. "10.99, 0, 0)");
  20. int [] updateCounts = stmt.executeBatch();
  21. this.con.commit();
  22. } catch(BatchUpdateException b) {
  23. JDBCTutorialUtilities.printBatchUpdateException(b);
  24. } catch(SQLException ex) {
  25. JDBCTutorialUtilities.printSQLException(ex);
  26. } finally {
  27. if (stmt != null) { stmt.close(); }
  28. this.con.setAutoCommit(true);
  29. }
  30. }

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

  1. this.con.setAutoCommit(false);

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

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

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

  1. 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对象启用自动提交模式。

  1. this.con.setAutoCommit(true);

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

执行参数化批量更新

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

  1. con.setAutoCommit(false);
  2. PreparedStatement pstmt = con.prepareStatement(
  3. "INSERT INTO COFFEES VALUES( " +
  4. "?, ?, ?, ?, ?)");
  5. pstmt.setString(1, "Amaretto");
  6. pstmt.setInt(2, 49);
  7. pstmt.setFloat(3, 9.99);
  8. pstmt.setInt(4, 0);
  9. pstmt.setInt(5, 0);
  10. pstmt.addBatch();
  11. pstmt.setString(1, "Hazelnut");
  12. pstmt.setInt(2, 49);
  13. pstmt.setFloat(3, 9.99);
  14. pstmt.setInt(4, 0);
  15. pstmt.setInt(5, 0);
  16. pstmt.addBatch();
  17. // ... and so on for each new
  18. // type of coffee
  19. int [] updateCounts = pstmt.executeBatch();
  20. con.commit();
  21. con.setAutoCommit(true);

处理批量更新例外

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

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

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

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

  1. public static void printBatchUpdateException(BatchUpdateException b) {
  2. System.err.println("----BatchUpdateException----");
  3. System.err.println("SQLState: " + b.getSQLState());
  4. System.err.println("Message: " + b.getMessage());
  5. System.err.println("Vendor: " + b.getErrorCode());
  6. System.err.print("Update counts: ");
  7. int [] updateCounts = b.getUpdateCounts();
  8. for (int i = 0; i < updateCounts.length; i++) {
  9. System.err.print(updateCounts[i] + " ");
  10. }
  11. }

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

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

  1. public void insertRow(String coffeeName, int supplierID,
  2. float price, int sales, int total)
  3. throws SQLException {
  4. Statement stmt = null;
  5. try {
  6. stmt = con.createStatement(
  7. ResultSet.TYPE_SCROLL_SENSITIVE
  8. ResultSet.CONCUR_UPDATABLE);
  9. ResultSet uprs = stmt.executeQuery(
  10. "SELECT * FROM " + dbName +
  11. ".COFFEES");
  12. uprs.moveToInsertRow();
  13. uprs.updateString("COF_NAME", coffeeName);
  14. uprs.updateInt("SUP_ID", supplierID);
  15. uprs.updateFloat("PRICE", price);
  16. uprs.updateInt("SALES", sales);
  17. uprs.updateInt("TOTAL", total);
  18. uprs.insertRow();
  19. uprs.beforeFirst();
  20. } catch (SQLException e ) {
  21. JDBCTutorialUtilities.printSQLException(e);
  22. } finally {
  23. if (stmt != null) { stmt.close(); }
  24. }
  25. }

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