文档

Java™教程
隐藏目录
使用CachedRowSetObjects
路径: JDBC数据库访问
课程: JDBC基础

使用CachedRowSetObjects

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

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

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

以下主题将被涵盖:

设置CachedRowSet对象

设置CachedRowSet对象涉及以下操作:

创建CachedRowSet对象

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

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

RowSetFactory factory = RowSetProvider.newFactory();
CachedRowSet crs = factory.createCachedRowSet();

对象crs的属性默认值与创建JdbcRowSet对象时的默认值相同。此外,它还被赋予了默认的SyncProvider实现RIOptimisticProvider的实例。

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

设置CachedRowSet属性

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

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

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

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

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对象:

crs.setCommand("select * from MERCH_INVENTORY");

设置关键列

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

int[] keys = {1};
crs.setKeyColumns(keys);

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

值得注意的是,方法setKeyColumns不会为属性设置值。在此示例中,它设置了字段keyCols的值。关键列在内部使用,因此在设置完关键列后,不需要再对它们做任何操作。您将在使用SyncResolver对象部分中看到关键列是如何以及何时使用的。

填充CachedRowSet对象

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

crs.execute();

crs中的数据是通过执行command属性中的查询语句产生的ResultSet对象中的数据。

不同之处在于CachedRowSet实现execute方法比JdbcRowSet实现更多。或者更准确地说,CachedRowSet对象的读取器(RowSetReader对象)做了更多的事情。

每个断开连接的RowSet对象都有一个分配给它的SyncProvider对象,这个SyncProvider对象提供RowSet对象的读取器。当创建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列的行中,对QUAN列的值加1:

        while (crs.next()) {
          System.out.println("找到商品 " + crs.getInt("ITEM_ID") + ":" +
                             crs.getString("ITEM_NAME"));
          if (crs.getInt("ITEM_ID") == 1235) {
            int currentQuantity = crs.getInt("QUAN") + 1;
            System.out.println("将数量更新为 " + currentQuantity);
            crs.updateInt("QUAN", currentQuantity + 1);
            crs.updateRow();
            // 同步行到数据库
            crs.acceptChanges(con);
          }
        } // 内部循环结束

插入和删除行

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

以下来自CachedRowSetSample.java的代码片段将在CachedRowSet对象crs中插入一行新数据:

crs.moveToInsertRow();
crs.updateInt("ITEM_ID", newItemId);
crs.updateString("ITEM_NAME", "桌布");
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中删除它:

while (crs.next()) {
    if (crs.getInt("ITEM_ID") == 12345) {
        crs.deleteRow();
        break;
    }
}

更新数据源

在对JdbcRowSet对象进行更改和对CachedRowSet对象进行更改之间有一个重要的区别。因为JdbcRowSet对象连接到其数据源,所以方法updateRowinsertRowdeleteRow可以同时更新JdbcRowSet对象和数据源。然而,在断开连接的RowSet对象的情况下,这些方法只能更新存储在CachedRowSet对象内存中的数据,而不能影响数据源。断开连接的RowSet对象必须调用方法acceptChanges才能将其更改保存到数据源中。在库存场景中,总部的应用程序将调用方法acceptChanges,以更新数据库中QUAN列的新值。

crs.acceptChanges();

Writer的作用

execute方法一样,acceptChanges方法是在后台完成工作的。不同的是,execute方法将工作委托给了RowSet对象的reader,而acceptChanges方法将工作委托给了RowSet对象的writer。在后台,writer会打开与数据库的连接,将对RowSet对象所做的更改更新到数据库中,然后关闭连接。

使用默认实现

困难在于可能会出现冲突。冲突是指在数据库中有另一个方在对应于RowSet对象中的值进行更新的情况。在数据库中应该保留哪个值?当发生冲突时,writer的处理方式取决于其实现方式,有很多可能性。在某些情况下,writer甚至不会检查冲突,只会将所有更改写入数据库。这就是RIXMLProvider实现的情况,它是由WebRowSet对象使用的。在另一种情况下,writer通过设置数据库锁来确保没有冲突,阻止其他人进行更改。

crs对象的writer是默认的SyncProvider实现RIOptimisticProviderRIOPtimisticProvider实现得名于它使用的乐观并发模型。该模型假设很少或根本没有冲突,因此不会设置数据库锁。writer会检查是否有任何冲突,如果没有冲突,则将对crs对象的更改写入数据库,这些更改将变为持久化。如果有任何冲突,默认情况是不将新的RowSet值写入数据库。

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

使用SyncResolver对象

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

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

try {
    crs.acceptChanges();
} catch (SyncProviderException spe) {
    SyncResolver resolver = spe.getSyncResolver();
}

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

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

    try {
        // ...
        // 同步新行回数据库。
        System.out.println("即将添加一行...");
        crs.acceptChanges(con);
        System.out.println("已添加一行...");
        this.viewTable(con);
        // ...
    } catch (SyncProviderException spe) {

      SyncResolver resolver = spe.getSyncResolver();

      Object crsValue; // RowSet对象中的值
      Object resolverValue; // SyncResolver对象中的值
      Object resolvedValue; // 要持久化的值

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

              // 比较crsValue和resolverValue以确定
              // 哪个应该是要持久化的解决值
              //
              // 本示例选择RowSet对象中的值crsValue持久化。

              resolvedValue = crsValue;

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

通知监听器

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

本节涵盖以下主题:

设置监听器

RowSet对象的监听器是实现RowSetListener接口中以下方法的组件:

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

作为应用程序员,利用通知机制的唯一要做的事情就是添加或删除监听器。下面这行代码的意思是每当crs对象的游标移动、crs中的值改变,或者crs整体获取新数据时,将通知BarGraph对象bar

crs.addRowSetListener(bar);

你也可以通过删除监听器来停止通知,如下面这行代码所示:

crs.removeRowSetListener(bar);

以Coffee Break场景为例,假设总部定期与数据库核对,以获取其在线销售的咖啡的最新价格列表。在这种情况下,监听器是Coffee Break网站上的PriceList对象priceList,它必须实现RowSetListener接口的cursorMovedrowChangedrowSetChanged方法。cursorMoved方法的实现可能为空,因为游标的位置不会影响priceList对象。另一方面,rowChangedrowSetChanged方法的实现必须确定已做出的更改并相应地更新priceList

通知机制的工作原理

触发任何RowSet事件的方法会自动通知所有注册的监听器。例如,任何移动游标的方法都会调用每个监听器的cursorMoved方法。同样地,execute方法会调用所有监听器的rowSetChanged方法,acceptChanges方法会调用所有监听器的rowChanged方法。

发送大量数据

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


上一页: 使用JdbcRowSet对象
下一页: 使用JoinRowSet对象