Java 中文官方教程 2022 版(三十五)

2024-05-24 14:56:13 浏览数 (1)

从结果集中检索和修改值

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

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

代码语言:javascript复制
  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。请注意,可以通过实现 Statement 接口的任何对象创建 ResultSet 对象,包括 PreparedStatementCallableStatementRowSet

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

本页涵盖以下主题:

  • ResultSet 接口
  • 从行中检索列值
  • 游标
  • 在 ResultSet 对象中更新行
  • 使用 Statement 对象进行批量更新
  • 在 ResultSet 对象中插入行

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_ONLYResultSet对象不能使用ResultSet接口进行更新。
  • CONCUR_UPDATABLEResultSet对象可以使用ResultSet接口进行更新。

默认的ResultSet并发性是CONCUR_READ_ONLY

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

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

游标可保持性

调用方法Connection.commit可以关闭在当前事务期间创建的ResultSet对象。然而,在某些情况下,这可能不是期望的行为。ResultSet属性holdability使应用程序可以控制在调用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

代码语言:javascript复制
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接口声明了获取器方法(例如,getBooleangetLong)用于从当前行检索列值。您可以使用列的索引号或别名或名称检索值。列索引通常更有效。列从 1 开始编号。为了最大的可移植性,应按照从左到右的顺序读取每行中的结果集列,并且每列只能读取一次。

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

代码语言:javascript复制
  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);
    }
  }

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

适当类型的获取器方法检索每列中的值。例如,在方法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方法移动光标。还有其他可用于移动光标的方法:

  • 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

代码语言:javascript复制
  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来更新数据库。

使用语句对象进行批量更新

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

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

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

代码语言:javascript复制
  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时,事务将不会自动提交或回滚。

代码语言:javascript复制
con.setAutoCommit(false);

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

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

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

代码语言:javascript复制
int[] updateCounts = stmt.executeBatch();

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

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

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

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

代码语言:javascript复制
con.setAutoCommit(true);

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

执行参数化的批量更新

还可以进行参数化的批量更新,如下面的代码片段所示,其中con是一个Connection对象:

代码语言:javascript复制
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);
处理批量更新异常

如果批处理中的一个 SQL 语句产生结果集(通常是查询),或者批处理中的一个 SQL 语句由于其他原因未成功执行,当调用方法executeBatch时,将会收到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循环打印每个更新计数:

代码语言:javascript复制
  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插入一行:

代码语言:javascript复制
  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方法将其移动到结果集中的第一行之前。如果应用程序的另一部分使用相同的结果集且游标仍指向插入行,则可能会出现意外结果。

使用预编译语句

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

本页涵盖以下主题:

  • 预编译语句概述
  • 创建一个 PreparedStatement 对象
  • 为 PreparedStatement 参数提供值

预编译语句概述

有时,使用PreparedStatement对象向数据库发送 SQL 语句更加方便。这种特殊类型的语句源自你已经了解的更一般的Statement类。

如果你想多次执行一个Statement对象,通常使用PreparedStatement对象可以减少执行时间。

PreparedStatement对象的主要特点是,与Statement对象不同,它在创建时就被赋予了一个 SQL 语句。这样做的好处是,在大多数情况下,这个 SQL 语句会立即发送到数据库管理系统(DBMS)中进行编译。因此,PreparedStatement对象不仅包含一个 SQL 语句,而且包含一个已经预编译过的 SQL 语句。这意味着当执行PreparedStatement时,DBMS 可以直接运行PreparedStatement的 SQL 语句,而无需先进行编译。

尽管你可以使用PreparedStatement对象执行没有参数的 SQL 语句,但通常你最常使用它们来执行带有参数的 SQL 语句。使用带有参数的 SQL 语句的优点是,你可以多次执行相同的语句,并每次执行时提供不同的值。以下部分中有相关示例。

然而,预编译语句最重要的优点是可以帮助防止 SQL 注入攻击。SQL 注入是一种恶意利用应用程序中使用客户端提供的数据的技术,用于在 SQL 语句中执行意外命令。攻击者通过提供经过特殊设计的字符串输入来欺骗 SQL 引擎,从而未经授权地访问数据库以查看或操纵受限数据。所有 SQL 注入技术都利用应用程序中的一个漏洞:未正确验证或未验证的字符串文字被连接到动态构建的 SQL 语句中,并被 SQL 引擎解释为代码。预编译语句始终将客户端提供的数据视为参数的内容,而不是 SQL 语句的一部分。有关更多信息,请参阅 Oracle 数据库文档中的数据库 PL/SQL 语言参考部分中的SQL 注入部分。

以下方法,CoffeesTable.updateCoffeeSales,将当前周内销售的咖啡磅数存储在每种咖啡的SALES列中,并更新每种咖啡的TOTAL列中销售的咖啡总磅数:

代码语言:javascript复制
  public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL   ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))

    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

创建 PreparedStatement 对象

以下创建了一个接受两个输入参数的PreparedStatement对象:

代码语言:javascript复制
    String updateString =
      "update COFFEES "   "set SALES = ? where COF_NAME = ?";
	// ...
    PreparedStatement updateSales = con.prepareStatement(updateString);

为 PreparedStatement 参数提供值

在执行PreparedStatement对象之前,必须在问号占位符的位置提供值(如果有的话)。通过调用PreparedStatement类中定义的 setter 方法来实现。以下语句为名为updateSalesPreparedStatement提供了两个问号占位符的值:

代码语言:javascript复制
updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());

这些 setter 方法的每个第一个参数指定了问号占位符。在这个例子中,setInt指定了第一个占位符,setString指定了第二个占位符。

在为参数设置了值之后,它会保留该值,直到被重置为另一个值,或者调用方法clearParameters。使用PreparedStatement对象updateSales,以下代码片段演示了在重置其参数值并保持另一个参数值不变后重用准备好的语句:

代码语言:javascript复制
// changes SALES column of French Roast
//row to 100

updateSales.setInt(1, 100);
updateSales.setString(2, "French_Roast");
updateSales.executeUpdate();

// changes SALES column of Espresso row to 100
// (the first parameter stayed 100, and the second
// parameter was reset to "Espresso")

updateSales.setString(2, "Espresso");
updateSales.executeUpdate();
使用循环设置值

通常可以通过使用for循环或while循环为输入参数设置值,从而使编码更加简单。

CoffeesTable.updateCoffeeSales方法使用 for-each 循环重复设置updateSalesupdateTotal中的值:

代码语言:javascript复制
for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
  updateSales.setInt(1, e.getValue().intValue());
  updateSales.setString(2, e.getKey());
  // ...
}

方法CoffeesTable.updateCoffeeSales接受一个参数,HashMapHashMap参数中的每个元素都包含当前周内销售的一种咖啡的名称和该种咖啡的磅数。for-each 循环遍历HashMap参数的每个元素,并设置updateSalesupdateTotal中相应的问号占位符。

执行 PreparedStatement 对象

Statement对象一样,要执行PreparedStatement对象,调用一个执行语句:如果查询只返回一个ResultSet(如SELECT SQL 语句),则调用executeQuery,如果查询不返回ResultSet(如UPDATE SQL 语句),则调用executeUpdate,如果查询可能返回多个ResultSet对象,则调用executeCoffeesTable.updateCoffeeSales(HashMap<String, Integer>)中的两个PreparedStatement对象都包含UPDATE SQL 语句,因此都通过调用executeUpdate来执行:

代码语言:javascript复制
updateSales.setInt(1, e.getValue().intValue());
updateSales.setString(2, e.getKey());
updateSales.executeUpdate();

updateTotal.setInt(1, e.getValue().intValue());
updateTotal.setString(2, e.getKey());
updateTotal.executeUpdate();
con.commit();

在执行updateSalesupdateTotals时,executeUpdate不需要提供参数;两个PreparedStatement对象已经包含要执行的 SQL 语句。

注意:在CoffeesTable.updateCoffeeSales的开头,自动提交模式被设置为 false:

代码语言:javascript复制
con.setAutoCommit(false);

因此,在调用方法commit之前,不会提交任何 SQL 语句。有关自动提交模式的更多信息,请参阅事务。

executeUpdate 方法的返回值

executeQuery返回一个包含发送到 DBMS 的查询结果的ResultSet对象,executeUpdate的返回值是一个int值,表示更新了表的多少行。例如,下面的代码展示了executeUpdate的返回值被赋给变量n

代码语言:javascript复制
updateSales.setInt(1, 50);
updateSales.setString(2, "Espresso");
int n = updateSales.executeUpdate();
// n = 1 because one row had a change in it

COFFEES已更新;值50替换了Espresso行中SALES列的值。这次更新影响了表中的一行,因此n等于 1。

当使用方法executeUpdate执行 DDL(数据定义语言)语句时,比如创建表时,它会返回值为int的 0。因此,在下面的代码片段中,用于执行创建表COFFEES的 DDL 语句时,n被赋值为 0:

代码语言:javascript复制
// n = 0
int n = executeUpdate(createTableCoffees); 

注意,当executeUpdate的返回值为 0 时,可能意味着两种情况之一:

  • 执行的语句是一个影响零行的更新语句。
  • 执行的语句是一个 DDL 语句。

使用事务

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

有时候你不希望一个语句生效,除非另一个语句完成。例如,当“咖啡休息时间”的老板更新每周销售的咖啡量时,老板还希望更新迄今为止的总销售量。然而,每周销售量和总销售量应该同时更新;否则,数据将不一致。确保两个动作都发生或两个动作都不发生的方法是使用事务。事务是一组一个或多个语句,作为一个单元执行,因此要么所有语句都执行,要么所有语句都不执行。

本页涵盖以下主题

  • 禁用自动提交模式
  • 提交事务
  • 使用事务保持数据完整性
  • 设置和回滚保存点
  • 释放保存点
  • 何时调用 rollback 方法

禁用自动提交模式

当创建连接时,它处于自动提交模式。这意味着每个单独的 SQL 语句都被视为一个事务,并在执行后立即自动提交。(更准确地说,默认情况下,SQL 语句在完成时提交,而不是在执行时。当所有结果集和更新计数都被检索时,语句完成。然而,在几乎所有情况下,语句在执行后立即完成,因此提交。)

允许将两个或多个语句分组为一个事务的方法是禁用自动提交模式。在以下代码中演示了这一点,其中con是一个活动连接:

代码语言:javascript复制
con.setAutoCommit(false);

提交事务

在禁用自动提交模式后,直到显式调用commit方法之前,不会提交任何 SQL 语句。在上一次调用commit方法之后执行的所有语句都包含在当前事务中,并作为一个单元一起提交。以下方法CoffeesTable.updateCoffeeSales,其中con是一个活动连接,演示了一个事务:

代码语言:javascript复制
  public void updateCoffeeSales(HashMap<String, Integer> salesForWeek) throws SQLException {
    String updateString =
      "update COFFEES set SALES = ? where COF_NAME = ?";
    String updateStatement =
      "update COFFEES set TOTAL = TOTAL   ? where COF_NAME = ?";

    try (PreparedStatement updateSales = con.prepareStatement(updateString);
         PreparedStatement updateTotal = con.prepareStatement(updateStatement))

    {
      con.setAutoCommit(false);
      for (Map.Entry<String, Integer> e : salesForWeek.entrySet()) {
        updateSales.setInt(1, e.getValue().intValue());
        updateSales.setString(2, e.getKey());
        updateSales.executeUpdate();

        updateTotal.setInt(1, e.getValue().intValue());
        updateTotal.setString(2, e.getKey());
        updateTotal.executeUpdate();
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
      if (con != null) {
        try {
          System.err.print("Transaction is being rolled back");
          con.rollback();
        } catch (SQLException excep) {
          JDBCTutorialUtilities.printSQLException(excep);
        }
      }
    }
  }

在这种方法中,连接con的自动提交模式被禁用,这意味着当调用commit方法时,两个准备好的语句updateSalesupdateTotal将一起提交。每当调用commit方法(无论是在自动提交模式启用时自动调用还是在禁用时显式调用),事务中语句导致的所有更改都将变为永久性。在这种情况下,这意味着哥伦比亚咖啡的SALESTOTAL列已更改为50(如果TOTAL之前为0),并将保留此值,直到它们通过另一个更新语句进行更改。

语句con.setAutoCommit(true);启用自动提交模式,这意味着每个语句再次在完成时自动提交。然后,您回到默认状态,无需自己调用commit方法。建议仅在事务模式下禁用自动提交模式。这样,您可以避免为多个语句保持数据库锁,从而增加与其他用户发生冲突的可能性。

使用事务保持数据完整性

除了将语句组合在一起作为一个单元执行外,事务还可以帮助保持表中数据的完整性。例如,想象一下,一个员工应该在COFFEES表中输入新的咖啡价格,但推迟了几天才这样做。与此同时,价格上涨了,今天所有者正在尝试输入更高的价格。员工最终开始输入现在已过时的价格,与此同时所有者正在尝试更新表。在插入过时价格后,员工意识到这些价格不再有效,并调用Connection方法rollback来撤消它们的影响。(rollback方法中止事务并将值恢复为尝试更新之前的值。)与此同时,所有者正在执行SELECT语句并打印新价格。在这种情况下,可能会打印出一个已经回滚到先前值的价格,使打印的价格不正确。

这种情况可以通过使用事务来避免,提供一定程度的保护,防止两个用户同时访问数据时出现冲突。

为了在事务期间避免冲突,数据库管理系统使用锁,这是一种阻止其他人访问事务正在访问的数据的机制。 (请注意,在自动提交模式下,每个语句都是一个事务,锁仅保留一个语句。)设置锁之后,锁将持续有效,直到事务提交或回滚。 例如,数据库管理系统可以锁定表的一行,直到对其进行的更新被提交。 这种锁的效果是防止用户获取脏读取,即在数据永久化之前读取值。 (访问尚未提交的更新值被视为脏读,因为可能将该值回滚到其先前的值。 如果读取后来被回滚的值,您将读取一个无效的值。)

锁是如何设置的取决于所谓的事务隔离级别,它可以从根本不支持事务到支持实施非常严格访问规则。

事务隔离级别的一个示例是TRANSACTION_READ_COMMITTED,它不允许在提交之后访问值。 换句话说,如果事务隔离级别设置为TRANSACTION_READ_COMMITTED,数据库管理系统不允许发生脏读取。 接口Connection包括五个值,表示您可以在 JDBC 中使用的事务隔离级别:

隔离级别

事务

脏读

不可重复读

幻读

TRANSACTION_NONE

不支持

不适用

不适用

不适用

TRANSACTION_READ_COMMITTED

支持

阻止

允许

允许

TRANSACTION_READ_UNCOMMITTED

支持

允许

允许

允许

TRANSACTION_REPEATABLE_READ

支持

阻止

阻止

允许

TRANSACTION_SERIALIZABLE

支持

阻止

阻止

阻止

当事务 A 检索一行,事务 B 随后更新该行,然后事务 A 稍后再次检索相同的行时,会发生不可重复读。 事务 A 两次检索相同的行,但看到不同的数据。

当事务 A 检索满足给定条件的一组行时,事务 B 随后插入或更新一行,使得该行现在满足事务 A 中的条件,然后事务 A 稍后重复条件检索时,会发生幻读。 事务 A 现在看到了一个额外的行。 这行被称为幻像。

通常,您不需要对事务隔离级别做任何处理;您可以只使用您的 DBMS 的默认隔离级别。默认事务隔离级别取决于您的 DBMS。例如,对于 Java DB,它是TRANSACTION_READ_COMMITTED。JDBC 允许您查找您的 DBMS 设置的事务隔离级别(使用Connection方法getTransactionIsolation),并且还允许您将其设置为另一个级别(使用Connection方法setTransactionIsolation)。

注意:JDBC 驱动程序可能不支持所有事务隔离级别。如果驱动程序不支持在调用setTransactionIsolation时指定的隔离级别,则驱动程序可以替换为更高、更严格的事务隔离级别。如果驱动程序无法替换为更高的事务级别,则会抛出SQLException。使用方法DatabaseMetaData.supportsTransactionIsolationLevel来确定驱动程序是否支持给定级别。

设置和回滚到 Savepoints

方法Connection.setSavepoint在当前事务中设置一个Savepoint对象。方法Connection.rollback被重载以接受一个Savepoint参数。

以下方法,CoffeesTable.modifyPricesByPercentage,通过百分比priceModifier提高特定咖啡的价格。但是,如果新价格大于指定价格maximumPrice,则价格将恢复到原始价格:

代码语言:javascript复制
  public void modifyPricesByPercentage(
    String coffeeName,
    float priceModifier,
    float maximumPrice) throws SQLException {
    con.setAutoCommit(false);
    ResultSet rs = null;
    String priceQuery = "SELECT COF_NAME, PRICE FROM COFFEES "  
                        "WHERE COF_NAME = ?";
    String updateQuery = "UPDATE COFFEES SET PRICE = ? "  
                         "WHERE COF_NAME = ?";
    try (PreparedStatement getPrice = con.prepareStatement(priceQuery, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY);
         PreparedStatement updatePrice = con.prepareStatement(updateQuery))
    {
      Savepoint save1 = con.setSavepoint();
      getPrice.setString(1, coffeeName);
      if (!getPrice.execute()) {
        System.out.println("Could not find entry for coffee named "   coffeeName);
      } else {
        rs = getPrice.getResultSet();
        rs.first();
        float oldPrice = rs.getFloat("PRICE");
        float newPrice = oldPrice   (oldPrice * priceModifier);
        System.out.printf("Old price of %s is $%.2f%n", coffeeName, oldPrice);
        System.out.printf("New price of %s is $%.2f%n", coffeeName, newPrice);
        System.out.println("Performing update...");
        updatePrice.setFloat(1, newPrice);
        updatePrice.setString(2, coffeeName);
        updatePrice.executeUpdate();
        System.out.println("nCOFFEES table after update:");
        CoffeesTable.viewTable(con);
        if (newPrice > maximumPrice) {
          System.out.printf("The new price, $%.2f, is greater "  
                            "than the maximum price, $%.2f. "  
                            "Rolling back the transaction...%n",
                            newPrice, maximumPrice);
          con.rollback(save1);
          System.out.println("nCOFFEES table after rollback:");
          CoffeesTable.viewTable(con);
        }
        con.commit();
      }
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    } finally {
      con.setAutoCommit(true);
    }
  }

以下语句指定了从getPrice查询生成的ResultSet对象的游标在调用commit方法时关闭。请注意,如果您的 DBMS 不支持ResultSet.CLOSE_CURSORS_AT_COMMIT,则此常量将被忽略:

代码语言:javascript复制
getPrice = con.prepareStatement(query, ResultSet.CLOSE_CURSORS_AT_COMMIT);

该方法首先通过以下语句创建一个Savepoint

代码语言:javascript复制
Savepoint save1 = con.setSavepoint();

该方法检查新价格是否大于maximumPrice值。如果是,则使用以下语句回滚事务:

代码语言:javascript复制
con.rollback(save1);

因此,当方法通过调用Connection.commit方法提交事务时,它不会提交任何已回滚其关联Savepoint的行;它将提交所有其他更新的行。

释放 Savepoints

方法Connection.releaseSavepointSavepoint对象作为参数,并将其从当前事务中移除。

释放 Savepoints 后,尝试在回滚操作中引用它会导致抛出SQLException。在事务提交或整个事务回滚时,已创建的任何保存点都会自动释放并在事务提交时变为无效,或者在回滚整个事务时变为无效。将事务回滚到保存点会自动释放并使其他在该保存点之后创建的保存点无效。

何时调用rollback方法

如前所述,调用方法rollback会终止事务并将修改的任何值返回到它们之前的值。如果您尝试在事务中执行一个或多个语句并收到SQLException,请调用方法rollback来结束事务并重新开始事务。这是唯一的方法来知道什么已经提交,什么尚未提交。捕获SQLException告诉您出现了问题,但它并不告诉您什么已经提交或尚未提交。因为您不能确定没有任何提交,调用方法rollback是确保的唯一方法。

方法CoffeesTable.updateCoffeeSales演示了一个事务,并包括一个调用方法rollbackcatch块。如果应用程序继续并使用事务的结果,catch块中对rollback方法的调用将阻止使用可能不正确的数据。

使用 RowSet 对象

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

JDBC RowSet对象以一种使其比结果集更灵活且更易于使用的方式保存表格数据。

Oracle 为RowSet的一些更受欢迎的用途定义了五个接口,并为这些RowSet接口提供了标准参考。在本教程中,您将学习如何使用这些参考实现。

这些版本的RowSet接口及其实现是为程序员提供方便的。程序员可以自由编写自己的javax.sql.RowSet接口版本,扩展这五个RowSet接口的实现,或编写自己的实现。然而,许多程序员可能会发现标准参考实现已经符合他们的需求,并将直接使用它们。

本节介绍了RowSet接口以及扩展此接口的以下接口:

  • JdbcRowSet
  • CachedRowSet
  • WebRowSet
  • JoinRowSet
  • FilteredRowSet

下面的主题包括:

  • RowSet 对象能做什么?
  • RowSet 对象的种类

RowSet 对象能做什么?

所有RowSet对象都源自ResultSet接口,因此共享其功能。JDBC RowSet对象的特殊之处在于它们添加了以下新功能:

  • 作为 JavaBeans 组件
  • 添加滚动或可更新性
作为 JavaBeans 组件

所有RowSet对象都是 JavaBeans 组件。这意味着它们具有以下特性:

  • 属性
  • JavaBeans 通知机制
属性

所有RowSet对象都有属性。属性是具有对应的 getter 和 setter 方法的字段。属性暴露给构建工具(例如随 IDE 一起提供的 JDveloper 和 Eclipse),使您可以直观地操作 bean。有关更多信息,请参阅 JavaBeans 教程中的属性课程。

JavaBeans 通知机制

RowSet对象使用 JavaBeans 事件模型,其中注册的组件在发生某些事件时会收到通知。对于所有RowSet对象,三个事件会触发通知:

  • 光标移动
  • 行的更新、插入或删除
  • 整个RowSet内容的更改

事件的通知发送给所有监听器,即已实现RowSetListener接口并已将自己添加到RowSet对象的组件列表中以在发生任何三个事件时收到通知的组件。

一个监听器可以是一个 GUI 组件,比如一个条形图。如果条形图正在跟踪一个RowSet对象中的数据,那么每当数据发生变化时,监听器都希望知道新的数据值。因此,监听器将实现RowSetListener方法来定义在特定事件发生时将执行什么操作。然后,监听器还必须添加到RowSet对象的监听器列表中。下面的代码行将条形图组件bg注册到RowSet对象rs

代码语言:javascript复制
rs.addListener(bg);

现在,每当光标移动、行发生变化或rs中的所有数据都更新时,bg都会收到通知。

添加滚动性或可更新性

一些数据库管理系统不支持可以滚动(可滚动)的结果集,有些不支持可以更新(可更新)的结果集。如果该数据库管理系统的驱动程序没有添加滚动或更新结果集的功能,您可以使用RowSet对象来实现。RowSet对象默认是可滚动和可更新的,因此通过将结果集的内容填充到RowSet对象中,您可以有效地使结果集可滚动和可更新。

RowSet 对象的种类

RowSet对象被认为是连接的或断开连接的。连接的RowSet对象使用 JDBC 驱动程序与关系数据库建立连接,并在其生命周期内保持该连接。断开连接的RowSet对象仅在从ResultSet对象中读取数据或将数据写回数据源时才与数据源建立连接。在从数据源读取数据或向数据源写入数据后,RowSet对象会断开与数据源的连接,从而变为“断开连接”。在其大部分生命周期中,断开连接的RowSet对象不与数据源建立连接,并且独立运行。接下来的两节将告诉您在连接或断开连接方面,RowSet对象可以做什么。

连接的 RowSet 对象

标准RowSet实现中只有一个连接的RowSet对象:JdbcRowSet。作为与数据库始终连接的对象,JdbcRowSet对象最类似于ResultSet对象,并且通常被用作包装器,使本来不可滚动和只读的ResultSet对象变得可滚动和可更新。

作为 JavaBeans 组件,例如,JdbcRowSet对象可以在 GUI 工具中用于选择 JDBC 驱动程序。JdbcRowSet对象可以这样使用,因为它实际上是驱动程序的包装器,该驱动程序获取了与数据库的连接。

断开连接的 RowSet 对象

另外四个实现是断开连接的RowSet实现。断开连接的RowSet对象具有连接的RowSet对象的所有功能,还具有仅适用于断开连接的RowSet对象的附加功能。例如,不必维护与数据源的连接使得断开连接的RowSet对象比JdbcRowSet对象或ResultSet对象更轻量级。断开连接的RowSet对象也是可序列化的,而且既可序列化又轻量级的组合使它们非常适合通过网络发送数据。它们甚至可以用于向 PDA 和手机等轻客户端发送数据。

CachedRowSet接口定义了所有断开连接的RowSet对象可用的基本功能。其他三个是CachedRowSet接口的扩展,提供更专业的功能。以下信息显示了它们之间的关系:

一个CachedRowSet对象具有JdbcRowSet对象的所有功能,还可以执行以下操作:

  • 获取到数据源的连接并执行查询
  • 从生成的ResultSet对象中读取数据并用该数据填充自身
  • 在断开连接时操作数据并对数据进行更改
  • 重新连接到数据源以将更改写回
  • 检查与数据源的冲突并解决这些冲突

一个WebRowSet对象具有CachedRowSet对象的所有功能,还可以执行以下操作:

  • 将自身写为 XML 文档
  • 读取描述WebRowSet对象的 XML 文档

一个JoinRowSet对象具有WebRowSet对象(因此也具有CachedRowSet对象)的所有功能,还可以执行以下操作:

  • 形成等效于SQL JOIN的操作而无需连接到数据源

一个FilteredRowSet对象同样具有WebRowSet对象(因此也具有CachedRowSet对象)的所有功能,还可以执行以下操作:

  • 应用过滤条件,以便只有选定的数据可见。这相当于在RowSet对象上执行查询,而无需使用查询语言或连接到数据源。

使用 JdbcRowSet 对象

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

JdbcRowSet 对象是一个增强的 ResultSet 对象。它与数据源保持连接,就像 ResultSet 对象一样。最大的区别在于它具有一组属性和监听器通知机制,使其成为一个 JavaBeans 组件。

JdbcRowSet 对象的主要用途之一是使一个 ResultSet 对象在没有这些功能的情况下可滚动和可更新。

本节涵盖以下主题:

  • 创建 JdbcRowSet 对象
  • 默认 JdbcRowSet 对象
  • 设置属性
  • 使用 JdbcRowSet 对象
  • 代码示例

创建 JdbcRowSet 对象

通过使用 RowSetProvider 类创建的 RowSetFactory 实例来创建 JdbcRowSet 对象。以下示例来自 JdbcRowSetSample.java

代码语言:javascript复制
    RowSetFactory factory = RowSetProvider.newFactory();

    try (JdbcRowSet jdbcRs = factory.createJdbcRowSet()) {
      jdbcRs.setUrl(this.settings.urlString);
      jdbcRs.setUsername(this.settings.userName);
      jdbcRs.setPassword(this.settings.password);
      jdbcRs.setCommand("select * from COFFEES");
      jdbcRs.execute();
      // ...

RowSetFactory 接口包含创建不同类型 RowSet 实现的方法:

  • createCachedRowSet
  • createFilteredRowSet
  • createJdbcRowSet
  • createJoinRowSet
  • createWebRowSet

默认 JdbcRowSet 对象

使用 RowSetFactory 实例创建 JdbcRowSet 对象时,新的 JdbcRowSet 对象将具有以下属性:

  • type: ResultSet.TYPE_SCROLL_INSENSITIVE(具有可滚动游标)
  • concurrency: ResultSet.CONCUR_UPDATABLE(可更新)
  • escapeProcessing: true(驱动程序将执行转义处理;启用转义处理时,驱动程序将扫描任何转义语法并将其转换为特定数据库理解的代码)
  • maxRows: 0(行数没有限制)
  • maxFieldSize: 0(列值的字节数没有限制;仅适用于存储 BINARYVARBINARYLONGVARBINARYCHARVARCHARLONGVARCHAR 值的列)
  • queryTimeout: 0(执行查询的时间没有限制)
  • showDeleted: false(已删除的行不可见)
  • transactionIsolation: Connection.TRANSACTION_READ_COMMITTED(仅读取已提交的数据)
  • typeMap: null(与此 RowSet 对象一起使用的 Connection 对象关联的类型映射为 null

你必须记住的主要事项是,JdbcRowSet 和所有其他 RowSet 对象都是可滚动和可更新的,除非你为这些属性设置了不同的值。

设置属性

章节 默认 JdbcRowSet 对象 列出了创建新 JdbcRowSet 对象时默认设置的属性。如果使用默认构造函数,必须在填充新的 JdbcRowSet 对象数据之前设置一些额外的属性。

要获取其数据,JdbcRowSet对象首先需要连接到数据库。以下四个属性保存用于获取数据库连接的信息。

  • username:用户作为访问权限的一部分向数据库提供的名称
  • password:用户的数据库密码
  • url:用户想要连接的数据库的 JDBC URL
  • datasourceName:用于检索已在 JNDI 命名服务中注册的DataSource对象的名称

你设置哪些属性取决于你如何进行连接。首选的方式是使用DataSource对象,但你可能无法将DataSource对象注册到 JNDI 命名服务中,这通常由系统管理员完成。因此,代码示例都使用DriverManager机制来获取连接,你需要使用url属性而不是datasourceName属性。

另一个你必须设置的属性是command属性。该属性是确定JdbcRowSet对象将保存什么数据的查询。例如,以下代码行使用查询设置了command属性,该查询生成一个包含表COFFEES中所有数据的ResultSet对象:

代码语言:javascript复制
jdbcRs.setCommand("select * from COFFEES");

在设置了command属性和连接所需的属性之后,通过调用execute方法,你就可以准备好通过数据填充jdbcRs对象。

代码语言:javascript复制
jdbcRs.execute();

execute方法在后台为你执行许多操作:

  • 它使用你分配给urlusernamepassword属性的值连接到数据库。
  • 它执行你在command属性中设置的查询。
  • 它将结果的数据从ResultSet对象读取到jdbcRs对象中。

使用 JdbcRowSet 对象

你在JdbcRowSet对象中更新、插入和删除行的方式与在可更新的ResultSet对象中更新、插入和删除行的方式相同。同样,你在JdbcRowSet对象中导航的方式与在可滚动的ResultSet对象中导航的方式相同。

Coffee Break 咖啡连锁店收购了另一家咖啡连锁店,现在拥有一个不支持结果集滚动或更新的传统数据库。换句话说,由这个传统数据库产生的任何ResultSet对象都没有可滚动的游标,其中的数据也无法修改。然而,通过创建一个从ResultSet对象中获取数据的JdbcRowSet对象,实际上可以使ResultSet对象可滚动和可更新。

如前所述,JdbcRowSet对象默认是可滚动和可更新的。因为其内容与ResultSet对象中的内容相同,对JdbcRowSet对象的操作等同于对ResultSet对象本身的操作。而且因为JdbcRowSet对象与数据库有持续连接,它对自己数据所做的更改也会应用到数据库中的数据。

本节涵盖以下主题:

  • 导航 JdbcRowSet 对象
  • 更新列值
  • 插入行
  • 删除行
导航 JdbcRowSet 对象

一个不可滚动的ResultSet对象只能使用next方法将其光标向前移动,并且只能从第一行向最后一行向前移动。然而,默认的JdbcRowSet对象可以使用ResultSet接口中定义的所有光标移动方法。

一个JdbcRowSet对象可以调用方法next,也可以调用任何其他ResultSet光标移动方法。例如,以下代码行将光标移动到jdbcRs对象的第四行,然后再移回第三行:

代码语言:javascript复制
jdbcRs.absolute(4);
jdbcRs.previous();

方法previous类似于方法next,可以在while循环中用于按顺序遍历所有行。不同之处在于你必须将光标移动到最后一行之后的位置,而previous将光标向前移动。

更新列值

你可以像更新ResultSet对象中的数据一样更新JdbcRowSet对象中的数据。

假设 Coffee Break 的老板想要提高一磅 Espresso 咖啡的价格。如果老板知道 Espresso 在jdbcRs对象的第三行,那么执行此操作的代码可能如下所示:

代码语言:javascript复制
jdbcRs.absolute(3);
jdbcRs.updateFloat("PRICE", 10.99f);
jdbcRs.updateRow();

该代码将光标移动到第三行,并将PRICE列的值更改为 10.99,然后使用新价格更新数据库。

调用方法updateRow会更新数据库,因为jdbcRs已经保持了与数据库的连接。对于断开连接的RowSet对象,情况是不同的。

插入行

如果 Coffee Break 连锁店的老板想要添加一种或多种咖啡到他所提供的咖啡中,那么老板需要为每种新咖啡在COFFEES表中添加一行,就像在JdbcRowSetSample.java中的以下代码片段中所做的那样。请注意,由于jdbcRs对象始终连接到数据库,向JdbcRowSet对象插入一行与向ResultSet对象插入一行相同:你移动光标到插入行,使用适当的更新方法为每列设置一个值,然后调用方法insertRow

代码语言:javascript复制
jdbcRs.moveToInsertRow();
jdbcRs.updateString("COF_NAME", "HouseBlend");
jdbcRs.updateInt("SUP_ID", 49);
jdbcRs.updateFloat("PRICE", 7.99f);
jdbcRs.updateInt("SALES", 0);
jdbcRs.updateInt("TOTAL", 0);
jdbcRs.insertRow();

jdbcRs.moveToInsertRow();
jdbcRs.updateString("COF_NAME", "HouseDecaf");
jdbcRs.updateInt("SUP_ID", 49);
jdbcRs.updateFloat("PRICE", 8.99f);
jdbcRs.updateInt("SALES", 0);
jdbcRs.updateInt("TOTAL", 0);
jdbcRs.insertRow();

当你调用方法insertRow时,新行将被插入jdbcRs对象并同时插入数据库。上述代码片段经历了这个过程两次,所以两行新行被插入jdbcRs对象和数据库。

删除行

与更新数据和插入新行一样,删除行对于JdbcRowSet对象和ResultSet对象来说是一样的。老板想要停止销售最后一行在jdbcRs对象中的 French Roast 无咖啡因咖啡。在以下代码行中,第一行将光标移动到最后一行,第二行删除了jdbcRs对象和数据库中的最后一行:

代码语言:javascript复制
jdbcRs.last();
jdbcRs.deleteRow();

代码示例

示例JdbcRowSetSample.java执行以下操作:

  • 创建一个使用执行检索COFFEES表中所有行的查询产生的ResultSet对象初始化的新JdbcRowSet对象
  • 将光标移动到COFFEES表的第三行,并更新该行的PRICE
  • 插入两行新行,一个是HouseBlend,另一个是HouseDecaf
  • 将光标移动到最后一行并将其删除

使用 CachedRowSetObjects

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

CachedRowSet对象很特殊,它可以在不连接到数据源的情况下运行,也就是说,它是一个断开连接RowSet对象。它的名字来源于它将数据存储(缓存)在内存中,这样它可以操作自己的数据而不是数据库中存储的数据。

CachedRowSet接口是所有断开连接的RowSet对象的超级接口,因此这里展示的所有内容也适用于WebRowSetJoinRowSetFilteredRowSet对象。

请注意,尽管CachedRowSet对象(以及从中派生的RowSet对象)的数据源几乎总是关系数据库,但CachedRowSet对象能够从以表格格式存储数据的任何数据源获取数据。例如,平面文件或电子表格可以是数据的来源。当为断开连接的RowSet对象实现RowSetReader对象以从这样的数据源读取数据时,这一点是正确的。CachedRowSet接口有一个从关系数据库读取数据的RowSetReader对象,因此在本教程中,数据源始终是数据库。

下面涵盖了以下主题:

  • 设置 CachedRowSet 对象
  • 填充 CachedRowSet 对象
  • Reader 的作用
  • 更新 CachedRowSet 对象
  • 更新数据源
  • Writer 的作用
  • 通知监听器
  • 发送大量数据

设置 CachedRowSet 对象

设置CachedRowSet对象涉及以下内容:

  • 创建 CachedRowSet 对象
  • 设置 CachedRowSet 属性
  • 设置关键列
创建 CachedRowSet 对象

通过使用RowSetProvider类创建的RowSetFactory实例来创建一个新的CachedRowSet对象。

以下示例来自CachedRowSetSample.java创建了一个CachedRowSet对象:

代码语言:javascript复制
RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();

对象crs的属性具有与创建时JdbcRowSet对象相同的默认值。此外,它已被分配默认SyncProvider实现RIOptimisticProvider的实例。

一个SyncProvider对象提供了一个RowSetReader对象(一个读取器)和一个RowSetWriter对象(一个写入器),一个断开连接的RowSet对象需要这些对象来从数据源读取数据或将数据写回数据源。读取器和写入器的功能将在后面的章节读取器的功能和写入器的功能中解释。需要记住的一点是,读取器和写入器完全在后台工作,因此它们如何工作的解释仅供参考。了解读取器和写入器的一些背景知识应该有助于你理解CachedRowSet接口中一些方法在后台做什么。

设置 CachedRowSet 属性

通常情况下,属性的默认值都是可以的,但是你可以通过调用适当的 setter 方法来更改属性的值。有一些没有默认值的属性,你必须自己设置。

为了获取数据,一个断开连接的RowSet对象必须能够连接到数据源,并且有一些选择要保存的数据的方法。以下属性保存了获取数据库连接所需的信息。

  • username: 用户在获取访问权限时向数据库提供的名称
  • password: 用户的数据库密码
  • url: 用户想要连接的数据库的 JDBC URL
  • datasourceName: 用于检索已经注册到 JNDI 命名服务的 DataSource 对象的名称

你必须设置哪些属性取决于你将如何建立连接。首选的方式是使用DataSource对象,但是你可能无法将DataSource对象注册到 JNDI 命名服务中,这通常由系统管理员完成。因此,代码示例都使用DriverManager机制来获取连接,你需要使用url属性而不是datasourceName属性。

以下代码行设置了usernamepasswordurl属性,以便使用DriverManager类获取连接。(你可以在你的 JDBC 驱动程序的文档中找到要设置为url属性值的 JDBC URL。)

代码语言:javascript复制
public void setConnectionProperties(
    String username, String password) {
    crs.setUsername(username);
    crs.setPassword(password);
    crs.setUrl("jdbc:mySubprotocol:mySubname");
    // ...

另一个你必须设置的属性是command属性。数据从ResultSet对象读入RowSet对象。产生该ResultSet对象的查询是command属性的值。例如,以下代码行使用一个查询设置了command属性,该查询产生一个包含表MERCH_INVENTORY中所有数据的ResultSet对象:

代码语言:javascript复制
crs.setCommand("select * from MERCH_INVENTORY");
设置关键列

如果要对 crs 对象进行任何更新并希望将这些更新保存到数据库中,必须设置另一个信息:关键列。关键列本质上与主键相同,因为它们指示唯一标识一行的一个或多个列。不同之处在于,主键设置在数据库中的表上,而关键列设置在特定的 RowSet 对象上。以下代码行将 crs 的关键列设置为第一列:

代码语言:javascript复制
int[] keys = {1};
crs.setKeyColumns(keys);

MERCH_INVENTORY 中的第一列是 ITEM_ID。它可以作为关键列,因为每个项目标识符都不同,因此唯一标识表 MERCH_INVENTORY 中的一行且仅一行。此外,该列在 MERCH_INVENTORY 表的定义中被指定为主键。方法 setKeyColumns 接受一个数组,以允许可能需要两个或更多列来唯一标识一行。

有趣的一点是,方法 setKeyColumns 不设置属性的值。在这种情况下,它为字段 keyCols 设置值。关键列在内部使用,因此在设置它们之后,您不再对其进行任何操作。您将在 使用 SyncResolver 对象 部分中看到关键列是如何使用的。

填充 CachedRowSet 对象

填充断开连接的 RowSet 对象比填充连接的 RowSet 对象需要更多的工作。幸运的是,额外的工作是在后台完成的。在完成了设置 CachedRowSet 对象 crs 的初步工作后,以下代码行填充了 crs

代码语言:javascript复制
crs.execute();

crs 中的数据是通过执行命令属性中的查询生成的 ResultSet 对象中的数据。

不同的是,CachedRowSet 实现的 execute 方法比 JdbcRowSet 实现做了更多的工作。更正确地说,CachedRowSet 对象的读取器,该方法委托其任务的对象,做了更多的工作。

每个断开连接的 RowSet 对象都分配了一个 SyncProvider 对象,并且这个 SyncProvider 对象提供了 RowSet 对象的 读取器(一个 RowSetReader 对象)。当创建 crs 对象时,它被用作默认的 CachedRowSetImpl 构造函数,除了为属性设置默认值外,还将 RIOptimisticProvider 实现的实例分配为默认的 SyncProvider 对象。

读取器的功能

当应用程序调用execute方法时,一个断开连接的RowSet对象的读取器在后台工作,将RowSet对象填充到数据中。新创建的CachedRowSet对象未连接到数据源,因此必须获取与该数据源的连接才能从中获取数据。默认的SyncProvider对象(RIOptimisticProvider)提供一个读取器,通过使用最近设置的用户名、密码和 JDBC URL 或数据源名称中的值来获取连接。然后读取器执行为命令设置的查询。它读取查询生成的ResultSet对象中的数据,将CachedRowSet对象填充到该数据中。最后,读取器关闭连接。

更新 CachedRowSet 对象

在 Coffee Break 场景中,所有者希望简化操作。所有者决定让仓库员工直接在 PDA(个人数字助理)中输入库存,从而避免让第二个人进行数据输入的容易出错的过程。在这种情况下,CachedRowSet对象是理想的,因为它轻量级、可序列化,并且可以在没有与数据源连接的情况下进行更新。

所有者将要求应用程序开发团队为仓库员工用于输入库存数据的 PDA 创建一个 GUI 工具。总部将创建一个填充有显示当前库存的表格的CachedRowSet对象,并通过互联网将其发送到 PDA。当仓库员工使用 GUI 工具输入数据时,该工具将每个条目添加到一个数组中,CachedRowSet对象将使用该数组在后台执行更新。完成库存后,PDA 将其新数据发送回总部,数据将上传到主服务器。

本节涵盖以下主题:

  • 更新列值
  • 插入和删除行
更新列值

更新CachedRowSet对象中的数据与更新JdbcRowSet对象中的数据完全相同。例如,来自CachedRowSetSample.java的以下代码片段将ITEM_ID列具有12345物品标识符的行中的QUAN列的值增加 1:

代码语言:javascript复制
        while (crs.next()) {
          System.out.println("Found item "   crs.getInt("ITEM_ID")   ": "  
                             crs.getString("ITEM_NAME"));
          if (crs.getInt("ITEM_ID") == 1235) {
            int currentQuantity = crs.getInt("QUAN")   1;
            System.out.println("Updating quantity to "   currentQuantity);
            crs.updateInt("QUAN", currentQuantity   1);
            crs.updateRow();
            // Syncing the row back to the DB
            crs.acceptChanges(con);
          }
        } // End of inner while
插入和删除行

就像更新列值一样,在CachedRowSet对象中插入和删除行的代码与JdbcRowSet对象相同。

来自CachedRowSetSample.java的以下摘录将新行插入到CachedRowSet对象crs中:

代码语言:javascript复制
crs.moveToInsertRow();
crs.updateInt("ITEM_ID", newItemId);
crs.updateString("ITEM_NAME", "TableCloth");
crs.updateInt("SUP_ID", 927);
crs.updateInt("QUAN", 14);
Calendar timeStamp;
timeStamp = new GregorianCalendar();
timeStamp.set(2006, 4, 1);
crs.updateTimestamp(
    "DATE_VAL",
    new Timestamp(timeStamp.getTimeInMillis()));
crs.insertRow();
crs.moveToCurrentRow();

如果总部决定停止储存某个特定物品,可能会直接删除咖啡本身的行。然而,在这种情况下,使用 PDA 的仓库员工也有能力删除它。以下代码片段找到ITEM_ID列中值为12345的行,并从CachedRowSet crs中删除它:

代码语言:javascript复制
while (crs.next()) {
    if (crs.getInt("ITEM_ID") == 12345) {
        crs.deleteRow();
        break;
    }
}

更新数据源

JdbcRowSet对象进行更改与对CachedRowSet对象进行更改之间存在重大差异。因为JdbcRowSet对象连接到其数据源,updateRowinsertRowdeleteRow方法可以更新JdbcRowSet对象和数据源。然而,在断开连接的RowSet对象的情况下,这些方法会更新CachedRowSet对象内存中存储的数据,但无法影响数据源。断开连接的RowSet对象必须调用acceptChanges方法才能将其更改保存到数据源。在库存场景中,总部的应用程序将调用acceptChanges方法以更新数据库中QUAN列的新值。

代码语言:javascript复制
crs.acceptChanges();

写入器的功能

execute方法类似,acceptChanges方法会在后台完成其工作。execute方法将其工作委托给RowSet对象的读取器,而acceptChanges方法将其任务委托给RowSet对象的写入器。在后台,写入器会打开与数据库的连接,使用RowSet对象所做的更改更新数据库,然后关闭连接。

使用默认实现

难点在于可能会出现冲突。冲突是指另一方已经更新了数据库中与RowSet对象中更新的值对应的值的情况。数据库中应该保留哪个值?当存在冲突时,写入器的处理方式取决于其如何实现,有许多可能性。在一个极端,写入器甚至不检查冲突,只是将所有更改写入数据库。这是RIXMLProvider实现的情况,它被WebRowSet对象使用。在另一端,写入器通过设置数据库锁来确保没有冲突,防止他人进行更改。

crs对象的写入器是默认SyncProvider实现提供的一个,名为RIOptimisticProviderRIOPtimisticProvider实现得名于其采用的乐观并发模型。该模型假设冲突会很少,甚至没有,因此不设置数据库锁。写入器会检查是否存在冲突,如果没有,则将对crs对象所做的更改写入数据库,这些更改变得持久。如果存在冲突,默认情况下不会将新的RowSet值写入数据库。

在这种情况下,默认行为非常有效。因为总部的人不太可能更改COF_INVENTORYQUAN列中的值,所以不会发生冲突。因此,在仓库中输入到crs对象中的值将被写入数据库,从而变得持久,这是期望的结果。

使用 SyncResolver 对象

然而,在其他情况下,可能存在冲突。为了适应这些情况,RIOPtimisticProvider实现提供了一个选项,让你查看冲突中的值,并决定哪些值应该持久化。这个选项就是使用SyncResolver对象。

当写入程序完成查找冲突并找到一个或多个冲突时,它会创建一个包含导致冲突的数据库值的SyncResolver对象。接下来,方法acceptChanges抛出一个SyncProviderException对象,应用程序可以捕获并用于检索SyncResolver对象。以下代码行检索SyncResolver对象resolver

代码语言:javascript复制
try {
    crs.acceptChanges();
} catch (SyncProviderException spe) {
    SyncResolver resolver = spe.getSyncResolver();
}

对象resolver是一个RowSet对象,复制了crs对象,只包含导致冲突的数据库中的值。所有其他列值都为 null。

使用resolver对象,你可以迭代其行以定位不为空且因此引起冲突的值。然后你可以定位crs对象中相同位置的值并进行比较。以下代码片段检索resolver并使用SyncResolver方法nextConflict来迭代具有冲突值的行。对象resolver获取每个冲突值的状态,如果是UPDATE_ROW_CONFLICT,表示crs在冲突发生时正在尝试更新,则resolver对象获取该值的行号。然后代码将crs对象的游标移动到相同的行。接下来,代码找到resolver对象中包含冲突值的行中的列,该列将是一个不为空的值。从resolvercrs对象中检索该列中的值后,你可以比较两者并决定哪个值应该持久化。最后,代码使用setResolvedValue方法在crs对象和数据库中设置该值,如下所示来自CachedRowSetSample.java的代码:

代码语言:javascript复制
    try {
        // ...
        // Syncing the new row back to the database.
        System.out.println("About to add a new row...");
        crs.acceptChanges(con);
        System.out.println("Added a row...");
        this.viewTable(con);
        // ...
    } catch (SyncProviderException spe) {

      SyncResolver resolver = spe.getSyncResolver();

      Object crsValue; // value in the RowSet object
      Object resolverValue; // value in the SyncResolver object
      Object resolvedValue; // value to be persisted

      while (resolver.nextConflict()) {

        if (resolver.getStatus() == SyncResolver.INSERT_ROW_CONFLICT) {
          int row = resolver.getRow();
          crs.absolute(row);

          int colCount = crs.getMetaData().getColumnCount();
          for (int j = 1; j <= colCount; j  ) {
            if (resolver.getConflictValue(j) != null) {
              crsValue = crs.getObject(j);
              resolverValue = resolver.getConflictValue(j);

              // Compare crsValue and resolverValue to determine
              // which should be the resolved value (the value to persist)
              //
              // This example chooses the value in the RowSet object,
              // crsValue, to persist.,

              resolvedValue = crsValue;

              resolver.setResolvedValue(j, resolvedValue);
            }
          }
        }
      }
    }

通知监听器

作为 JavaBeans 组件意味着RowSet对象在发生某些事情时可以通知其他组件。例如,如果RowSet对象中的数据发生变化,RowSet对象可以通知感兴趣的方。这种通知机制的好处在于,作为应用程序员,你只需添加或移除将被通知的组件。

本节涵盖以下主题:

  • 设置监听器
  • 通知工作原理
设置监听器

一个RowSet对象的监听器是一个实现RowSetListener接口的组件,该接口包括以下方法:

  • cursorMoved:定义了当RowSet对象中的游标移动时,监听器将执行什么操作(如果有的话)。
  • rowChanged:定义了监听器在一行中一个或多个列值发生变化、插入了一行或删除了一行时将执行的操作(如果有的话)。
  • rowSetChanged:定义了监听器在RowSet对象被填充新数据时将执行的操作(如果有的话)。

一个可能想要成为监听器的组件示例是一个将RowSet对象中的数据制成图表的BarGraph对象。随着数据的变化,BarGraph对象可以更新自身以反映新数据。

作为一个应用程序员,利用通知机制的唯一要做的事情就是添加或移除监听器。下面这行代码意味着每当crs对象的光标移动,crs中的值发生变化,或者整个crs获取新数据时,BarGraph对象bar都会收到通知:

代码语言:javascript复制
crs.addRowSetListener(bar);

你也可以通过移除监听器来停止通知,就像下面这行代码所做的那样:

代码语言:javascript复制
crs.removeRowSetListener(bar);

使用咖啡休息场景,假设总部定期检查数据库以获取在线销售的咖啡最新价格列表。在这种情况下,监听器是咖啡休息网站上的PriceList对象priceList,它必须实现RowSetListener方法cursorMovedrowChangedrowSetChangedcursorMoved方法的实现可能是什么都不做,因为光标的位置不会影响priceList对象。另一方面,rowChangedrowSetChanged方法的实现必须确定所做的更改,并相应地更新priceList

通知如何工作

导致任何RowSet事件的方法会自动通知所有注册的监听器。例如,任何移动光标的方法也会调用每个监听器的cursorMoved方法。同样,execute方法会调用所有监听器的rowSetChanged方法,而acceptChanges会调用所有监听器的rowChanged方法。

发送大量数据

该方法CachedRowSetSample.java演示了如何将数据分成较小的部分发送。

使用 JoinRowSet 对象

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

JoinRowSet实现允许您在RowSet对象之间创建 SQL JOIN,当它们未连接到数据源时。这很重要,因为它节省了必须创建一个或多个连接的开销。

下面涵盖的主题有:

  • 创建 JoinRowSet 对象
  • 添加 RowSet 对象
  • 管理匹配列

JoinRowSet接口是CachedRowSet接口的子接口,因此继承了CachedRowSet对象的功能。这意味着JoinRowSet对象是一个断开连接的RowSet对象,可以在不始终连接到数据源的情况下运行。

创建 JoinRowSet 对象

JoinRowSet对象充当 SQL JOIN的持有者。来自JoinSample.java的以下示例显示了如何创建JoinRowSet对象:

代码语言:javascript复制
    RowSetFactory factory = RowSetProvider.newFactory();  
    try (CachedRowSet coffees = factory.createCachedRowSet();
         CachedRowSet suppliers = factory.createCachedRowSet();
         JoinRowSet jrs = factory.createJoinRowSet()) {
      coffees.setCommand("SELECT * FROM COFFEES");
      // Set connection parameters for the CachedRowSet coffees
      coffees.execute();

      suppliers.setCommand("SELECT * FROM SUPPLIERS");
      // Set connection parameters for the CachedRowSet suppliers      suppliers.execute();      

      jrs.addRowSet(coffees, "SUP_ID");
      jrs.addRowSet(suppliers, "SUP_ID");

      // ...

变量jrs在添加RowSet对象之前不包含任何内容。

添加 RowSet 对象

任何RowSet对象都可以添加到JoinRowSet对象中,只要它可以成为 SQL JOIN的一部分。JdbcRowSet对象始终连接到其数据源,可以添加,但通常通过直接与数据源操作而不是通过添加到JoinRowSet对象来成为JOIN的一部分。提供JoinRowSet实现的目的是使断开连接的RowSet对象能够成为JOIN关系的一部分。

The owner of The Coffee Break chain of coffee houses wants to get a list of the coffees he buys from Acme, Inc. In order to do this, the owner will have to get information from two tables, COFFEES and SUPPLIERS. In the database world before RowSet technology, programmers would send the following query to the database:

代码语言:javascript复制
String query =
    "SELECT COFFEES.COF_NAME "  
    "FROM COFFEES, SUPPLIERS "  
    "WHERE SUPPLIERS.SUP_NAME = Acme.Inc. "  
    "and "  
    "SUPPLIERS.SUP_ID = COFFEES.SUP_ID";

RowSet技术的世界中,您可以在不向数据源发送查询的情况下实现相同的结果。您可以将包含两个表中数据的RowSet对象添加到JoinRowSet对象中。然后,因为所有相关数据都在JoinRowSet对象中,您可以对其执行查询以获取所需数据。

来自JoinSample.testJoinRowSet的以下代码片段创建了两个CachedRowSet对象,coffees中包含来自表COFFEES的数据,suppliers中包含来自表SUPPLIERS的数据。coffeessuppliers对象必须连接到数据库以执行其命令并获取数据,但完成后,它们不必重新连接以形成JOIN

代码语言:javascript复制
    try (CachedRowSet coffees = factory.createCachedRowSet();
         CachedRowSet suppliers = factory.createCachedRowSet();
         JoinRowSet jrs = factory.createJoinRowSet()) {
      coffees.setCommand("SELECT * FROM COFFEES");
      coffees.setUsername(settings.userName);
      coffees.setPassword(settings.password);
      coffees.setUrl(settings.urlString);
      coffees.execute();

      suppliers.setCommand("SELECT * FROM SUPPLIERS");
      suppliers.setUsername(settings.userName);
      suppliers.setPassword(settings.password);
      suppliers.setUrl(settings.urlString);
      suppliers.execute();  
	  // ...

管理匹配列

查看SUPPLIERS表,您会发现 Acme, Inc. 的标识号为 101。在COFFEES表中,供应商标识号为 101 的咖啡有哥伦比亚咖啡和哥伦比亚无咖啡因。这两个表的信息可以进行连接,因为两个表都有一个名为SUP_ID的列。在 JDBC 的RowSet技术中,JOIN所基于的列SUP_ID被称为匹配列

添加到JoinRowSet对象中的每个RowSet对象必须具有匹配列,即JOIN所基于的列。有两种方法可以为RowSet对象设置匹配列。第一种方法是将匹配列传递给JoinRowSet方法addRowSet,如下面的代码所示:

代码语言:javascript复制
jrs.addRowSet(coffees, "SUP_ID");

此行代码将coffeesCachedRowSet添加到jrs对象中,并将coffeesSUP_ID列设置为匹配列。

此时,jrs中只有coffees。下一个添加到jrs中的RowSet对象必须能够与coffees进行JOIN,这对于suppliers是成立的,因为两个表都有SUP_ID列。下面的代码将suppliers添加到jrs中,并将SUP_ID列设置为匹配列。

代码语言:javascript复制
jrs.addRowSet(suppliers, "SUP_ID");

现在jrs包含了coffeessuppliers之间的JOIN,所有者可以从中获取 Acme, Inc.供应的咖啡的名称。因为代码没有设置JOIN的类型,jrs保持内部 JOIN,这是默认值。换句话说,jrs中的一行组合了coffees中的一行和suppliers中的一行。它包含了coffees中的列以及suppliers中的列,对于COFFEES.SUP_ID列的值与SUPPLIERS.SUP_ID列的值匹配的行。以下代码打印出由 Acme, Inc.供应的咖啡的名称,其中String supplierName等于Acme, Inc.请注意,这是可能的,因为JoinRowSet对象jrs中现在包括了来自suppliersSUP_NAME列和来自coffeesCOF_NAME列。

代码语言:javascript复制
      System.out.println("Coffees bought from "   supplierName   ": ");
      while (jrs.next()) {
        if (jrs.getString("SUP_NAME").equals(supplierName)) { 
          String coffeeName = jrs.getString(1);
          System.out.println("     "   coffeeName);
        }
      }

这将产生类似以下的输出:

代码语言:javascript复制
Coffees bought from Acme, Inc.:
     Colombian
     Colombian_Decaf

JoinRowSet接口提供了用于设置将形成的JOIN类型的常量,但目前实现的唯一类型是JoinRowSet.INNER_JOIN

使用 FilteredRowSet 对象

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

FilteredRowSet对象允许您减少在RowSet对象中可见的行数,以便您只处理与您正在进行的工作相关的数据。您决定要对数据设置什么限制(如何“过滤”数据),并将该过滤器应用到FilteredRowSet对象上。换句话说,FilteredRowSet对象只显示符合您设置限制的数据行。JdbcRowSet对象始终与其数据源连接,可以通过向数据源发送仅选择您想要查看的列和行的查询来进行此过滤。查询的WHERE子句定义了过滤条件。FilteredRowSet对象提供了一种让断开连接的RowSet对象进行此过滤的方法,而无需在数据源上执行查询,从而避免必须连接到数据源并向其发送查询。

例如,假设咖啡休息连锁店已经在美利坚合众国各地发展到数百家店铺,并且所有店铺都列在名为COFFEE_HOUSES的表中。业主希望通过一款不需要与数据库系统保持持久连接的咖啡店比较应用程序来衡量仅加利福尼亚州的店铺的成功。这种比较将关注销售商品与销售咖啡饮料的盈利能力以及其他各种成功指标,并将按照咖啡饮料销售额、商品销售额和总销售额对加利福尼亚州的店铺进行排名。由于COFFEE_HOUSES表有数百行数据,如果将搜索的数据量减少到仅包含STORE_ID列中指示加利福尼亚州的行,这些比较将更快更容易进行。

这正是FilteredRowSet对象通过提供以下功能来解决的问题:

  • 能够根据设定的条件限制可见的行
  • 能够选择哪些数据可见而无需连接到数据源

下面的主题包括:

  • 在谓词对象中定义过滤条件
  • 创建 FilteredRowSet 对象
  • 创建和设置谓词对象
  • 使用新的谓词对象设置 FilteredRowSet 对象以进一步过滤数据
  • 更新 FilteredRowSet 对象
  • 插入或更新行
  • 删除所有过滤器以使所有行可见
  • 删除行

在谓词对象中定义过滤条件

要设置FilteredRowSet对象中哪些行可见的条件,您需要定义一个实现Predicate接口的类。使用此类创建的对象将初始化为以下内容:

  • 值必须落在的范围的高端
  • 值必须落在的范围的低端
  • 列名或列号是必须落在高低边界设置的值范围内的值所在的列的列名或列号

请注意,值范围是包容的,这意味着边界处的值包括在范围内。例如,如果范围的高端为 100,低端为 50,则 50 的值被视为在范围内。49 不在范围内。同样,100 在范围内,但 101 不在范围内。

符合业主想要比较加利福尼亚店铺的情景,必须编写一个实现Predicate接口的过滤位于加利福尼亚的 Coffee Break 咖啡店的类。没有一种正确的方法来做这件事,这意味着在编写实现的方式上有很大的自由度。例如,您可以随意命名类及其成员,并以任何实现方式编写构造函数和三个评估方法,以实现所需的结果。

列出所有咖啡店的表名为COFFEE_HOUSES,有数百行。为了使事情更易管理,此示例使用了少得多的行数的表,足以演示如何进行过滤。

STORE_ID中的值是一个int值,表示咖啡店所在的州等信息。例如,以 10 开头的值表示该州是加利福尼亚。以 32 开头的STORE_ID值表示俄勒冈州,以 33 开头的表示华盛顿州。

以下类,StateFilter,实现了Predicate接口:

代码语言:javascript复制
public class StateFilter implements Predicate {

    private int lo;
    private int hi;
    private String colName = null;
    private int colNumber = -1;

    public StateFilter(int lo, int hi, int colNumber) {
        this.lo = lo;
        this.hi = hi;
        this.colNumber = colNumber;
    }

    public StateFilter(int lo, int hi, String colName) {
        this.lo = lo;
        this.hi = hi;
        this.colName = colName;
    }

    public boolean evaluate(Object value, String columnName) {
        boolean evaluation = true;
        if (columnName.equalsIgnoreCase(this.colName)) {
            int columnValue = ((Integer)value).intValue();
            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            } else {
                evaluation = false;
            }
        }
        return evaluation;
    }

    public boolean evaluate(Object value, int columnNumber) {

        boolean evaluation = true;

        if (this.colNumber == columnNumber) {
            int columnValue = ((Integer)value).intValue();
            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            } else {
                evaluation = false;
            }
        }
        return evaluation;
    }

    public boolean evaluate(RowSet rs) {

        CachedRowSet frs = (CachedRowSet)rs;
        boolean evaluation = false;

        try {
            int columnValue = -1;

            if (this.colNumber > 0) {
                columnValue = frs.getInt(this.colNumber);
            } else if (this.colName != null) {
                columnValue = frs.getInt(this.colName);
            } else {
                return false;
            }

            if ((columnValue >= this.lo)
                &&
                (columnValue <= this.hi)) {
                evaluation = true;
            }
        } catch (SQLException e) {
            JDBCTutorialUtilities.printSQLException(e);
            return false;
        } catch (NullPointerException npe) {
            System.err.println("NullPointerException caught");
            return false;
        }
        return evaluation;
    }
}

这是一个非常简单的实现,检查由colNamecolNumber指定的列中的值是否在lohi的范围内,包括边界。以下代码行,来自FilteredRowSetSample.java,创建了一个过滤器,仅允许STORE_ID列值指示介于 10000 和 10999 之间的行,这表示加利福尼亚位置:

代码语言:javascript复制
StateFilter myStateFilter = new StateFilter(10000, 10999, 1);

请注意,刚刚定义的StateFilter类仅适用于一列。可以通过将每个参数数组而不是单个值来使其适用于两个或更多列。例如,Filter对象的构造函数可能如下所示:

代码语言:javascript复制
public Filter2(Object [] lo, Object [] hi, Object [] colNumber) {
    this.lo = lo;
    this.hi = hi;
    this.colNumber = colNumber;
}

colNumber对象中的第一个元素表示将根据其在lohi中的第一个元素进行检查的第一列。由colNumber指示的第二列中的值将与lohi中的第二个元素进行检查,依此类推。因此,这三个数组中的元素数量应该相同。下面的代码是evaluate(RowSet rs)方法的一个实现示例,用于Filter2对象,其中参数是数组:

代码语言:javascript复制
public boolean evaluate(RowSet rs) {
    CachedRowSet crs = (CachedRowSet)rs;
    boolean bool1;
    boolean bool2;
    for (int i = 0; i < colNumber.length; i  ) {

        if ((rs.getObject(colNumber[i] >= lo [i]) &&
            (rs.getObject(colNumber[i] <= hi[i]) {
            bool1 = true;
        } else {
            bool2 = true;
        }

        if (bool2) {
            return false;
        } else {
            return true;
        }
    }
}

使用Filter2实现的优势在于可以使用任何Object类型的参数,并且可以检查一个或多个列,而无需编写另一个实现。但是,您必须传递一个Object类型,这意味着您必须将原始类型转换为其Object类型。例如,如果您使用int值作为lohi,则必须在将其传递给构造函数之前将int值转换为Integer对象。String对象已经是Object类型,因此您无需转换它们。

创建 FilteredRowSet 对象

使用从RowSetProvider类创建的RowSetFactory实例来创建FilteredRowSet对象。以下是来自FilteredRowSetSample.java的示例:

代码语言:javascript复制
    RowSetFactory factory = RowSetProvider.newFactory();
    try (FilteredRowSet frs = factory.createFilteredRowSet()) {
      // ...

与其他断开连接的RowSet对象一样,frs对象必须从表格数据源(在参考实现中是关系数据库)中填充自身数据。下面来自FilteredRowSetSample.java的代码片段设置了连接到数据库以执行其命令所需的属性。请注意,此代码使用DriverManager类建立连接,这是为了方便起见。通常情况下,最好使用已在实现了 Java 命名和目录接口(JNDI)的命名服务中注册的DataSource对象。

代码语言:javascript复制
    RowSetFactory factory = RowSetProvider.newFactory();
    try (FilteredRowSet frs = factory.createFilteredRowSet()){
      frs.setCommand("SELECT * FROM COFFEE_HOUSES");
      frs.setUsername(settings.userName);
      frs.setPassword(settings.password);
      frs.setUrl(settings.urlString);
      frs.execute();
      // ...

下面的代码行将frs对象填充了存储在COFFEE_HOUSE表中的数据:

代码语言:javascript复制
frs.execute();

execute方法通过调用frsRowSetReader对象在后台执行各种操作,该对象创建连接,执行frs的命令,将frs填充为从生成的ResultSet对象中获取的数据,并关闭连接。请注意,如果COFFEE_HOUSES表的行数超过了frs对象一次性内存中可以容纳的行数,将使用CachedRowSet的分页方法。

在这种情况下,Coffee Break 的所有者将在办公室完成前述任务,然后导入或下载存储在frs对象中的信息到咖啡馆比较应用程序中。从现在开始,frs对象将独立运行,不再依赖于与数据源的连接。

创建和设置谓词对象

现在FilteredRowSet对象frs包含了 Coffee Break 商店的列表,您可以为frs对象中可见的行数设置选择条件以缩小范围。

以下代码行使用先前定义的StateFilter类来创建对象myStateFilter,该对象检查STORE_ID列以确定哪些商店位于加利福尼亚(如果其 ID 号在 10000 到 10999 之间,则商店位于加利福尼亚):

代码语言:javascript复制
StateFilter myStateFilter = new StateFilter(10000, 10999, 1);

以下行将myStateFilter设置为frs的过滤器。

代码语言:javascript复制
frs.setFilter(myStateFilter);

要进行实际过滤,您调用next方法,该方法在参考实现中调用您先前实现的Predicate.evaluate方法的适当版本。

如果返回值为true,则该行将可见;如果返回值为false,则该行将不可见。

使用新的Predicate对象设置 FilteredRowSet 对象以进一步过滤数据

您可以串行设置多个过滤器。第一次调用setFilter方法并传递一个Predicate对象时,您已经应用了该过滤器中的过滤条件。在对每一行调用next方法后,只有满足过滤条件的行才会可见,然后您可以再次调用setFilter,传递不同的Predicate对象。尽管一次只设置一个过滤器,但效果是两个过滤器都会累积应用。

例如,所有者通过将stateFilter设置为frsPredicate对象来检索加利福尼亚的 Coffee Break 商店列表。现在所有者想要比较加利福尼亚的两个城市中的商店,旧金山(表COFFEE_HOUSES中的 SF)和洛杉矶(表中的 LA)。首先要做的是编写一个Predicate实现,用于过滤旧金山或洛杉矶的商店:

代码语言:javascript复制
public class CityFilter implements Predicate {

    private String[] cities;
    private String colName = null;
    private int colNumber = -1;

    public CityFilter(String[] citiesArg, String colNameArg) {
        this.cities = citiesArg;
        this.colNumber = -1;
        this.colName = colNameArg;
    }

    public CityFilter(String[] citiesArg, int colNumberArg) {
        this.cities = citiesArg;
        this.colNumber = colNumberArg;
        this.colName = null;
    }

    public boolean evaluate Object valueArg, String colNameArg) {

        if (colNameArg.equalsIgnoreCase(this.colName)) {
            for (int i = 0; i < this.cities.length; i  ) {
                if (this.cities[i].equalsIgnoreCase((String)valueArg)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean evaluate(Object valueArg, int colNumberArg) {

        if (colNumberArg == this.colNumber) {
            for (int i = 0; i < this.cities.length; i  ) {
                if (this.cities[i].equalsIgnoreCase((String)valueArg)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean evaluate(RowSet rs) {

        if (rs == null) return false;

        try {
            for (int i = 0; i < this.cities.length; i  ) {

                String cityName = null;

                if (this.colNumber > 0) {
                    cityName = (String)rs.getObject(this.colNumber);
                } else if (this.colName != null) {
                    cityName = (String)rs.getObject(this.colName);
                } else {
                    return false;
                }

                if (cityName.equalsIgnoreCase(cities[i])) {
                    return true;
                }
            }
        } catch (SQLException e) {
            return false;
        }
        return false;
    }
}

来自FilteredRowSetSample.java的以下代码片段设置了新的过滤器,并遍历frs中的行,打印出CITY列包含旧金山或洛杉矶的行。请注意,frs当前仅包含商店位于加利福尼亚的行,因此当将过滤器更改为另一个Predicate对象时,state对象的条件仍然有效。接下来的代码将过滤器设置为CityFilter对象cityCityFilter实现使用数组作为构造函数的参数,以说明可以如何完成:

代码语言:javascript复制
  public void testFilteredRowSet() throws SQLException {

    StateFilter myStateFilter = new StateFilter(10000, 10999, 1);
    String[] cityArray = { "SF", "LA" };

    CityFilter myCityFilter = new CityFilter(cityArray, 2);

	RowSetFactory factory = RowSetProvider.newFactory();

    try (FilteredRowSet frs = factory.createFilteredRowSet()){
      frs.setCommand("SELECT * FROM COFFEE_HOUSES");
      frs.setUsername(settings.userName);
      frs.setPassword(settings.password);
      frs.setUrl(settings.urlString);
      frs.execute();

      System.out.println("nBefore filter:");
      FilteredRowSetSample.viewTable(this.con);

      System.out.println("nSetting state filter:");
      frs.beforeFirst();
      frs.setFilter(myStateFilter);
      this.viewFilteredRowSet(frs);

      System.out.println("nSetting city filter:");
      frs.beforeFirst();
      frs.setFilter(myCityFilter);
      this.viewFilteredRowSet(frs);
    } catch (SQLException e) {
      JDBCTutorialUtilities.printSQLException(e);
    }
  }

输出应包含每个位于加利福尼亚旧金山或洛杉矶的商店的行。如果有一行中CITY列包含 LA 且STORE_ID列包含 40003,则不会包含在列表中,因为在将过滤器设置为state时已经被过滤掉(40003 不在 10000 到 10999 的范围内)。

更新 FilteredRowSet 对象

您可以对FilteredRowSet对象进行更改,但前提是该更改不违反当前生效的任何过滤条件。例如,如果新值或值在过滤条件内,则可以插入新行或更改现有行中的一个或多个值。

插入或更新行

假设两家新的 Coffee Break 咖啡馆刚刚开业,所有者希望将它们添加到所有咖啡馆的列表中。如果要插入的行不符合当前累积的过滤条件,则将阻止其添加。

frs对象的当前状态是设置了StateFilter对象,然后设置了CityFilter对象。因此,frs目前仅显示符合两个过滤器条件的行。同样重要的是,除非符合两个过滤器的条件,否则无法向frs对象添加行。以下代码片段尝试向frs对象插入两行新行,其中一个行中的STORE_IDCITY列的值都符合条件,另一个行中的STORE_ID的值不符合过滤条件,但CITY列的值符合:

代码语言:javascript复制
frs.moveToInsertRow();
frs.updateInt("STORE_ID", 10101);
frs.updateString("CITY", "SF");
frs.updateLong("COF_SALES", 0);
frs.updateLong("MERCH_SALES", 0);
frs.updateLong("TOTAL_SALES", 0);
frs.insertRow();

frs.updateInt("STORE_ID", 33101);
frs.updateString("CITY", "SF");
frs.updateLong("COF_SALES", 0);
frs.updateLong("MERCH_SALES", 0);
frs.updateLong("TOTAL_SALES", 0);
frs.insertRow();
frs.moveToCurrentRow();

如果使用方法next迭代frs对象,你会发现旧金山、加利福尼亚州的新咖啡馆的行,但不会看到华盛顿州旧金山的商店的行。

删除所有过滤器以使所有行可见

所有者可以通过取消过滤器来添加华盛顿州的商店。没有设置过滤器,frs对象中的所有行再次可见,任何位置的商店都可以添加到商店列表中。以下代码行取消当前过滤器,有效地使先前在frs对象上设置的两个Predicate实现无效。

代码语言:javascript复制
frs.setFilter(null);

删除行

如果所有者决定关闭或出售其中一家 Coffee Break 咖啡馆,所有者将希望从COFFEE_HOUSES表中删除它。只要行可见,所有者就可以删除表现不佳的咖啡馆的行。

例如,假设刚刚使用参数 null 调用了方法setFilter,则frs对象上没有设置任何过滤器。这意味着所有行都是可见的,因此可以删除。然而,在设置了过滤掉除加利福尼亚州以外任何州的StateFilter对象myStateFilter之后,只有位于加利福尼亚州的商店才能被删除。当为frs对象设置了CityFilter对象myCityFilter时,只有旧金山、加利福尼亚州或洛杉矶、加利福尼亚州的咖啡馆可以被删除,因为它们是唯一可见的行。

使用 WebRowSet 对象

docs.oracle.com/javase/tutorial/jdbc/basics/webrowset.html

WebRowSet对象非常特殊,因为除了提供CachedRowSet对象的所有功能外,它还可以将自身写入为 XML 文档,并且还可以读取该 XML 文档以将自身转换回WebRowSet对象。由于 XML 是异构企业之间可以相互通信的语言,因此它已成为 Web 服务通信的标准。因此,WebRowSet对象通过使 Web 服务能够以 XML 文档的形式从数据库发送和接收数据来填补了一个真正的需求。

下面涵盖了以下主题:

  • 创建和填充 WebRowSet 对象
  • 将 WebRowSet 对象写入和读取为 XML
  • XML 文档中的内容是什么
  • 对 WebRowSet 对象进行更改

Coffee Break 公司已扩展到在线销售咖啡。用户可以从 Coffee Break 网站按磅订购咖啡。价格列表定期更新,通过从公司数据库获取最新信息。本节演示了如何通过WebRowSet对象和单个方法调用将价格数据发送为 XML 文档。

创建和填充 WebRowSet 对象

通过使用RowSetFactory的实例创建一个新的WebRowSet对象,该实例是从RowSetProvider类创建的,用于创建一个WebRowSet对象。以下示例来自WebRowSetSample.java

代码语言:javascript复制
    RowSetFactory factory = RowSetProvider.newFactory();  
    try (WebRowSet priceList = factory.createWebRowSet();
         // ...
    ) {	  
      // ...

尽管priceList对象尚无数据,但它具有BaseRowSet对象的默认属性。其SyncProvider对象首先设置为RIOptimisticProvider实现,这是所有断开连接的RowSet对象的默认值。但是,WebRowSet实现会将SyncProvider对象重置为RIXMLProvider实现。

您可以使用RowSetFactory的实例创建一个WebRowSet对象,该实例是从RowSetProvider类创建的。有关更多信息,请参见使用 RowSetFactory 接口中的使用 JdbcRowSet 对象。

Coffee Break 总部定期向其网站发送价格列表更新。关于WebRowSet对象的这些信息将展示您可以通过 XML 文档发送最新价格列表的一种方式。

价格列表包括来自表COFFEES的列COF_NAMEPRICE中的数据。以下代码片段设置所需的属性,并使用价格列表数据填充priceList对象:

代码语言:javascript复制
      int[] keyCols = {1};
      priceList.setUsername(settings.userName);
      priceList.setPassword(settings.password);
      priceList.setUrl(settings.urlString);
      priceList.setCommand("select COF_NAME, PRICE from COFFEES");
      priceList.setKeyColumns(keyCols);

      // Populate the WebRowSet
      priceList.execute();

此时,除了默认属性之外,priceList对象还包含来自COFFEES表中COF_NAMEPRICE列的数据,以及关于这两列的元数据。

将 WebRowSet 对象写入和读取为 XML

要将WebRowSet对象写入 XML 文档,请调用方法writeXml。要将该 XML 文档的内容读入WebRowSet对象,请调用方法readXml。这两种方法都在后台执行其工作,除了结果之外,其他都对您不可见。

使用writeXml方法

方法writeXml将调用它的WebRowSet对象作为表示其当前状态的 XML 文档写入。它将这个 XML 文档写入您传递给它的流。流可以是一个OutputStream对象,比如一个FileOutputStream对象,或者一个Writer对象,比如一个FileWriter对象。如果您向方法writeXml传递一个OutputStream对象,您将以字节形式写入,可以处理所有类型的数据;如果您向它传递一个Writer对象,您将以字符形式写入。以下代码演示将WebRowSet对象priceList作为 XML 文档写入FileOutputStream对象oStream

代码语言:javascript复制
java.io.FileOutputStream oStream =
    new java.io.FileOutputStream("priceList.xml");
priceList.writeXml(oStream);

以下代码将代表priceList的 XML 文档写入FileWriter对象writer,而不是写入OutputStream对象。FileWriter类是一个方便的用于向文件写入字符的类。

代码语言:javascript复制
java.io.FileWriter writer =
    new java.io.FileWriter("priceList.xml");
priceList.writeXml(writer);

方法writeXml的另外两个版本允许您在将其写入流之前,使用ResultSet对象的内容填充WebRowSet对象。在下面的代码行中,方法writeXmlResultSet对象rs的内容读入priceList对象,然后将priceList作为 XML 文档写入FileOutputStream对象oStream

代码语言:javascript复制
priceList.writeXml(rs, oStream);

在下一行代码中,writeXml方法将priceList填充为rs的内容,但将 XML 文档写入FileWriter对象,而不是写入OutputStream对象:

代码语言:javascript复制
priceList.writeXml(rs, writer);
使用readXml方法

方法readXml解析 XML 文档以构造 XML 文档描述的WebRowSet对象。与方法writeXml类似,您可以向readXml传递一个InputStream对象或Reader对象,从中读取 XML 文档。

代码语言:javascript复制
java.io.FileInputStream iStream =
    new java.io.FileInputStream("priceList.xml");
priceList.readXml(iStream);

java.io.FileReader reader = new
    java.io.FileReader("priceList.xml");
priceList.readXml(reader);

请注意,您可以将 XML 描述读入一个新的WebRowSet对象中,或者读入调用writeXml方法的相同WebRowSet对象中。在从总部发送价格列表信息到网站的情况下,您将使用一个新的WebRowSet对象,如下面的代码所示:

代码语言:javascript复制
WebRowSet recipient = new WebRowSetImpl();
java.io.FileReader reader =
    new java.io.FileReader("priceList.xml");
recipient.readXml(reader);

XML 文档中包含什么

RowSet对象不仅包含它们所包含的数据,还包含有关其列的属性和元数据。因此,表示WebRowSet对象的 XML 文档除了数据外还包括其他信息。此外,XML 文档中的数据包括当前值和原始值。 (回想一下,原始值是在对数据进行最近更改之前立即存在的值。这些值对于检查数据库中的相应值是否已更改是必要的,从而创建关于应该持久化哪个值的冲突:您放入RowSet对象的新值还是其他人放入数据库中的新值。)

WebRowSet XML 模式本身是一个 XML 文档,定义了表示WebRowSet对象的 XML 文档将包含什么以及必须以什么格式呈现。发送方和接收方都使用此模式,因为它告诉发送方如何编写 XML 文档(表示WebRowSet对象的文档)以及接收方如何解析 XML 文档。由于实际的写入和读取是由writeXmlreadXml方法的实现在内部完成的,因此作为用户,您不需要了解 WebRowSet XML 模式文档中的内容。

XML 文档以分层结构包含元素和子元素。以下是描述WebRowSet对象的 XML 文档中的三个主要元素:

  • 属性
  • 元数据
  • 数据

元素标签表示元素的开始和结束。例如,<properties>标签表示属性元素的开始,</properties>标签表示其结束。<map/>标签是一种简写方式,表示尚未为地图子元素(属性元素中的一个子元素)分配值。以下示例 XML 文档使用间距和缩进使其更易于阅读,但在实际的 XML 文档中不使用这些,其中间距不表示任何内容。

接下来的三个部分向您展示了在示例WebRowSetSample中创建的WebRowSet priceList对象的三个主要元素包含什么。

属性

priceList对象上调用writeXml方法将生成描述priceList的 XML 文档。此 XML 文档的属性部分将如下所示:

代码语言:javascript复制
<properties>
  <command>
    select COF_NAME, PRICE from COFFEES
  </command>
  <concurrency>1008</concurrency>
  <datasource><null/></datasource>
  <escape-processing>true</escape-processing>
  <fetch-direction>1000</fetch-direction>
  <fetch-size>0</fetch-size>
  <isolation-level>2</isolation-level>
  <key-columns>
    <column>1</column>
  </key-columns>
  <map>
  </map>
  <max-field-size>0</max-field-size>
  <max-rows>0</max-rows>
  <query-timeout>0</query-timeout>
  <read-only>true</read-only>
  <rowset-type>
    ResultSet.TYPE_SCROLL_INSENSITIVE
  </rowset-type>
  <show-deleted>false</show-deleted>
  <table-name>COFFEES</table-name>
  <url>jdbc:mysql://localhost:3306/testdb</url>
  <sync-provider>
    <sync-provider-name>
      com.sun.rowset.providers.RIOptimisticProvider
    </sync-provider-name>
    <sync-provider-vendor>
      Sun Microsystems Inc.
    </sync-provider-vendor>
    <sync-provider-version>
      1.0
    </sync-provider-version>
    <sync-provider-grade>
      2
    </sync-provider-grade>
    <data-source-lock>1</data-source-lock>
  </sync-provider>
</properties>

请注意,某些属性没有值。例如,datasource属性用<datasource/>标签表示,这是一种简写方式,表示<datasource></datasource>。没有给出值,因为已设置url属性。建立的任何连接将使用此 JDBC URL 完成,因此不需要设置DataSource对象。此外,usernamepassword属性未列出,因为它们必须保持机密。

元数据

描述WebRowSet对象的 XML 文档的元数据部分包含有关该WebRowSet对象中列的信息。以下显示了描述priceList对象的WebRowSet对象的此部分的外观。因为priceList对象有两列,描述它的 XML 文档有两个<column-definition>元素。每个<column-definition>元素都有子元素提供有关所描述列的信息。

代码语言:javascript复制
<metadata>
  <column-count>2</column-count>
  <column-definition>
    <column-index>1</column-index>
    <auto-increment>false</auto-increment>
    <case-sensitive>false</case-sensitive>
    <currency>false</currency>
    <nullable>0</nullable>
    <signed>false</signed>
    <searchable>true</searchable>
    <column-display-size>
      32
    </column-display-size>
    <column-label>COF_NAME</column-label>
    <column-name>COF_NAME</column-name>
    <schema-name></schema-name>
    <column-precision>32</column-precision>
    <column-scale>0</column-scale>
    <table-name>coffees</table-name>
    <catalog-name>testdb</catalog-name>
    <column-type>12</column-type>
    <column-type-name>
      VARCHAR
    </column-type-name>
  </column-definition>
  <column-definition>
    <column-index>2</column-index>
    <auto-increment>false</auto-increment>
    <case-sensitive>true</case-sensitive>
    <currency>false</currency>
    <nullable>0</nullable>
    <signed>true</signed>
    <searchable>true</searchable>
    <column-display-size>
      12
    </column-display-size>
    <column-label>PRICE</column-label>
    <column-name>PRICE</column-name>
    <schema-name></schema-name>
    <column-precision>10</column-precision>
    <column-scale>2</column-scale>
    <table-name>coffees</table-name>
    <catalog-name>testdb</catalog-name>
    <column-type>3</column-type>
    <column-type-name>
      DECIMAL
    </column-type-name>
  </column-definition>
</metadata>

从这个元数据部分,你可以看到每行中有两列。第一列是COF_NAME,它保存VARCHAR类型的值。第二列是PRICE,它保存REAL类型的值,等等。请注意,列类型是数据源中使用的数据类型,而不是 Java 编程语言中的类型。要获取或更新COF_NAME列中的值,你可以使用getStringupdateString方法,驱动程序会将其转换为VARCHAR类型,就像通常做的那样。

数据

数据部分提供了WebRowSet对象每行中每列的值。如果你已经填充了priceList对象并且没有对其进行任何更改,XML 文档的数据元素将如下所示。在下一节中,你将看到当你修改priceList对象中的数据时,XML 文档如何变化。

每一行都有一个<currentRow>元素,因为priceList有两列,所以每个<currentRow>元素包含两个<columnValue>元素。

代码语言:javascript复制
<data>
  <currentRow>
    <columnValue>Colombian</columnValue>
    <columnValue>7.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>
      Colombian_Decaf
    </columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>Espresso</columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>French_Roast</columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>French_Roast_Decaf</columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
</data>

对 WebRowSet 对象进行更改

WebRowSet对象进行更改的方式与对CachedRowSet对象相同。然而,与CachedRowSet对象不同,WebRowSet对象会跟踪更新、插入和删除,以便writeXml方法可以写入当前值和原始值。接下来的三个部分演示了对数据进行更改,并展示了描述WebRowSet对象的 XML 文档在每次更改后的样子。关于 XML 文档,你无需做任何事情;任何对它的更改都是自动进行的,就像写入和读取 XML 文档一样。

插入行

如果 Coffee Break 连锁店的老板想要在价格表中添加一种新的咖啡,代码可能如下所示:

代码语言:javascript复制
priceList.absolute(3);
priceList.moveToInsertRow();
priceList.updateString(COF_NAME, "Kona");
priceList.updateFloat(PRICE, 8.99f);
priceList.insertRow();
priceList.moveToCurrentRow();

在参考实现中,插入会立即在当前行之后进行。在前面的代码片段中,当前行是第三行,因此新行将在第三行之后添加,并成为新的第四行。为了反映这个插入,XML 文档将在<data>元素中第三个<currentRow>元素之后添加以下<insertRow>元素。

<insertRow>元素看起来类似于以下内容。

代码语言:javascript复制
<insertRow>
  <columnValue>Kona</columnValue>
  <columnValue>8.99</columnValue>
</insertRow>

删除行

所有者认为浓缩咖啡销售不足,应从 The Coffee Break 商店出售的咖啡中移除。因此,所有者希望从价格表中删除浓缩咖啡。浓缩咖啡位于priceList对象的第三行,因此以下代码行将其删除:

代码语言:javascript复制
priceList.absolute(3); priceList.deleteRow();

以下<deleteRow>元素将出现在 XML 文档数据部分的第二行之后,表示第三行已被删除。

代码语言:javascript复制
<deleteRow>
  <columnValue>Espresso</columnValue>
  <columnValue>9.99</columnValue>
</deleteRow>

修改行

所有者进一步决定哥伦比亚咖啡的价格太贵,想将其降至每磅6.99。以下代码设置了哥伦比亚咖啡的新价格,即第一行为每磅6.99:

代码语言:javascript复制
priceList.first();
priceList.updateFloat(PRICE, 6.99);

XML 文档将在<updateRow>元素中反映这一变化,给出新值。第一列的值未更改,因此仅有第二列的<updateValue>元素:

代码语言:javascript复制
<currentRow>
  <columnValue>Colombian</columnValue>
  <columnValue>7.99</columnValue>
  <updateRow>6.99</updateRow>
</currentRow>

此时,通过插入一行、删除一行和修改一行,priceList对象的 XML 文档将如下所示:

代码语言:javascript复制
<data>
  <insertRow>
    <columnValue>Kona</columnValue>
    <columnValue>8.99</columnValue>
  </insertRow>
  <currentRow>
    <columnValue>Colombian</columnValue>
    <columnValue>7.99</columnValue>
    <updateRow>6.99</updateRow>
  </currentRow>
  <currentRow>
    <columnValue>
      Colombian_Decaf
    </columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <deleteRow>
    <columnValue>Espresso</columnValue>
    <columnValue>9.99</columnValue>
  </deleteRow>
  <currentRow>
    <columnValue>French_Roast</columnValue>
    <columnValue>8.99</columnValue>
  </currentRow>
  <currentRow>
    <columnValue>
      French_Roast_Decaf
    </columnValue>
    <columnValue>9.99</columnValue>
  </currentRow>
</data>

WebRowSet 代码示例

示例WebRowSetSample.java展示了本页面描述的所有功能。

0 人点赞