Using CachedRowSetObjects

CachedRowSet对象的特殊之处在于它可以在不连接到其数据源的情况下运行,即它是断开连接的RowSet对象。它之所以得名,是因为它将数据存储(缓存)在内存中,这样它就可以对自己的数据进行操作,而不是对数据库中存储的数据进行操作。

CachedRowSetinterface是所有断开连接的RowSet对象的superinterface,因此此处演示的所有内容也适用于WebRowSetJoinRowSetFilteredRowSet对象。

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

涵盖以下主题:

设置 CachedRowSet 对象

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

创建 CachedRowSet 对象

您可以通过以下不同方式创建新的CachedRowSet对象:

注意 :或者,您可以使用 JDBC 驱动程序的CachedRowSet实现中的构造函数。但是,RowSetinterface的实现将与参考实现不同。这些实现将具有不同的名称和构造函数。例如,CachedRowSetinterface的 Oracle JDBC 驱动程序实现称为oracle.jdbc.rowset.OracleCachedRowSet

使用默认构造函数

创建CachedRowSet对象的方法之一是调用参考实现中定义的默认构造函数,如以下代码行所示:

CachedRowSet crs = new CachedRowSetImpl();

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

SyncProvider对象提供RowSetReader对象(读取器)和RowSetWriter对象(写入器),断开连接的RowSet对象需要从其数据源读取数据或将数据写回到其数据源。稍后在Reader 做什么Writer 做什么部分中说明 Reader 和 Writer 的工作。要记住的一件事是,Reader 和 Writer 完全是在后台工作,因此对他们如何工作的解释仅供参考。对 Reader 和 Writer 有一些了解,应该可以帮助您了解CachedRowSetinterface中定义的某些方法在后台所做的工作。

设置 CachedRowSet 属性

通常,属性的默认值可以保持原样,但是您可以通过调用适当的 setter 方法来更改属性的值。您必须自行设置一些没有默认值的属性。

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

  • username:用户为获得访问权限而提供给数据库的名称

  • password:用户的数据库密码

  • url:用户要连接到的数据库的 JDBC URL

  • datasourceName:用于检索已向 JNDI 命名服务注册的 DataSource 对象的名称

您必须设置这些属性中的哪一个取决于您将如何构建连接。首选方法是使用DataSource对象,但是使用 JNDI 命名服务注册DataSource对象可能不切实际,这通常是由系统 管理 员完成的。因此,所有代码示例均使用DriverManager机制来获取连接,为此您使用url属性而不是datasourceName属性。

下面的代码行设置usernamepasswordurl属性,以便可以使用DriverManager类获得连接。 (在 JDBC 驱动程序的文档中,您将找到 JDBC URL 设置为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属性,该查询生成一个ResultSet对象,该对象包含表MERCH_INVENTORY中的所有数据:

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对象中的数据。

不同之处在于,execute方法的CachedRowSet实现比JdbcRowSet实现的功能大得多。或更正确地说,该方法执行的CachedRowSet对象的读取器委派了其任务,并且执行了更多操作。

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

Reader 做什么

当应用程序调用方法execute时,断开连接的RowSet对象的读取器在后台进行工作,以向RowSet对象填充数据。新创建的CachedRowSet对象未连接到数据源,因此必须获得与该数据源的连接才能从中获取数据。默认SyncProvider对象(RIOptimisticProvider)的参考实现提供了一种读取器,该读取器通过使用为用户名,密码以及 JDBC URL 或数据源名称(无论是最近设置的值)设置的值来获取连接。然后,阅读器执行命令查询集。它读取查询产生的ResultSet对象中的数据,并使用该数据填充CachedRowSet对象。最后,阅读器关闭连接。

更新 CachedRowSet 对象

在“茶歇”方案中,所有者希望简化操作。所有者决定让仓库中的员工将库存直接 ImportingPDA(个人数字助理)中,从而避免了由第二个人进行数据 Importing 的容易出错的过程。 CachedRowSet对象在这种情况下是理想的,因为它是轻量级的,可序列化的,并且可以在不连接数据源的情况下进行更新。

所有者将让应用程序开发团队为 PDA 创建一个 GUI 工具,仓库员工将使用该工具来 Importing 库存数据。总部将创建一个CachedRowSet对象,并在该表中填充当前清单,并将其通过 Internet 发送到 PDA。当仓库员工使用 GUI 工具 Importing 数据时,该工具会将每个条目添加到一个数组中,CachedRowSet对象将使用该数组在后台执行更新。完成清单后,PDA 将其新数据发送回总部,然后将数据上传到总部。

本节涵盖以下主题:

更新列值

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

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();
        // Synchronizing the row
        // back to the DB
        crs.acceptChanges(con);
    }

插入和删除行

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

CachedRowSetSample.java的以下摘录将新行插入CachedRowSet对象crs

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删除:

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对象的读取器,而方法acceptChanges将其任务委托给RowSet对象的写入器。在后台,编写器打开与数据库的连接,使用对RowSet对象所做的更改更新数据库,然后关闭连接。

使用默认实现

困难在于可能会发生冲突。冲突是指另一方已更新数据库中与RowSet对象中更新的值相对应的值的情况。数据库中应保留哪个值?发生冲突时,作者所做的工作取决于冲突的实现方式,并且有很多可能性。在频谱的一端,编写器甚至不检查冲突,而只是将所有更 Rewrite 入数据库。 WebRowSet对象使用的RIXMLProvider实现就是这种情况。另一方面,编写者通过设置数据库锁来防止其他人进行更改,从而确保不存在冲突。

crs对象的 writer 是默认SyncProvider实现RIOptimisticProvider提供的对象。 RIOPtimisticProvider实现的名称源于它使用开放式并发模型的事实。该模型假定几乎没有冲突,因此不设置数据库锁。编写器检查是否存在任何冲突,如果没有冲突,则将对crs对象所做的更 Rewrite 入数据库,然后这些更改将持久化。如果存在任何冲突,则默认为不将新的RowSet值写入数据库。

在这种情况下,默认行为效果很好。因为总部的任何人都不可能更改COF_INVENTORYQUAN列中的值,所以不会发生冲突。结果,在仓库中 Importingcrs对象的值将被写入数据库,因此将是持久的,这是期望的结果。

使用 SyncResolver 对象

但是,在其他情况下,可能存在冲突。为了适应这些情况,RIOPtimisticProvider实现提供了一个选项,可让您查看冲突中的值并确定应保留的值。此选项是使用SyncResolver对象。

当编写者完成查找冲突并找到一个或多个冲突时,它将创建一个SyncResolver对象,该对象包含导致冲突的数据库值。接下来,方法acceptChanges引发SyncProviderException对象,应用程序可以catch该对象并使用它来检索SyncResolver对象。以下代码行检索SyncResolver对象resolver

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

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

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

try {
    crs.acceptChanges();
} catch (SyncProviderException spe) {
    SyncResolver resolver = spe.getSyncResolver();
  
    // value in crs
    Object crsValue;
  
    // value in the SyncResolver object
    Object resolverValue; 
  
    // value to be persistent
    Object resolvedValue; 

    while (resolver.nextConflict()) {
        if (resolver.getStatus() ==
            SyncResolver.UPDATE_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 the value to be
                    // persistent

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

Notifying Listeners

成为 JavaBeans 组件意味着RowSet对象可以在发生某些事情时通知其他组件。例如,如果RowSet对象中的数据发生更改,则RowSet对象可以将该更改通知相关方。关于此通知机制的好处是,作为应用程序程序员,您要做的就是添加或删除将要通知的组件。

本节涵盖以下主题:

设置监听器

RowSet对象的侦听器是一个通过RowSetListenerinterface实现以下方法的组件:

  • cursorMoved:定义RowSet对象中的光标移动时侦听器将执行的操作(如果有)。

  • rowChanged:定义当行中的一个或多个列值已更改,已插入行或已删除行时,侦听器将执行的操作(如果有)。

  • rowSetChanged:定义在RowSet对象已填充新数据时,侦听器将执行的操作(如果有)。

可能想成为侦听器的组件的一个示例是BarGraph对象,该对象以图形表示RowSet对象中的数据。随着数据的更改,BarGraph对象可以更新自身以反映新数据。

作为应用程序程序员,要利用通知机制,您唯一要做的就是添加或删除侦听器。以下代码行意味着,每次crs对象的光标移动,crs中的值发生更改或crs整体获取新数据时,BarGraph对象bar都会收到通知:

crs.addRowSetListener(bar);

您还可以通过删除侦听器来停止通知,如下面的代码行所示:

crs.removeRowSetListener(bar);

使用 Coffee Break 场景,假设总部定期与数据库核对,以获取其在线销售的咖啡的最新价格表。在这种情况下,侦听器是 Coffee Break 网站上的PriceList对象priceList,该对象必须实现RowSetListener方法cursorMovedrowChangedrowSetChangedcursorMoved方法的实现可能什么也不做,因为游标的位置不会影响priceList对象。另一方面,rowChangedrowSetChanged方法的实现必须确定已进行了哪些更改并相应地更新priceList

通知的工作方式

在参考实现中,导致任何RowSet事件的方法都会自动通知所有已注册的侦听器。例如,任何移动光标的方法也会在每个侦听器上调用方法cursorMoved。同样,方法execute在所有侦听器上调用方法rowSetChanged,并且acceptChanges在所有侦听器上调用rowChanged

发送大量数据

示例代码CachedRowSetSample.testCachedRowSet演示了如何以较小的片段发送数据。