# 使用 CachedRowSetObjects

> 原文： [https://docs.oracle.com/javase/tutorial/jdbc/basics/cachedrowset.html](https://docs.oracle.com/javase/tutorial/jdbc/basics/cachedrowset.html)

`CachedRowSet`对象的特殊之处在于它可以在不连接到其数据源的情况下运行，也就是说，它是 _ 断开 _ `RowSet`对象。它的名称来源于它将数据存储（缓存）在内存中，以便它可以在自己的数据上运行，而不是在数据库中存储的数据上运行。

`CachedRowSet`接口是所有断开连接的`RowSet`对象的超接口，因此此处演示的所有内容也适用于`WebRowSet`，`JoinRowSet`和`FilteredRowSet`对象。

请注意，虽然`CachedRowSet`对象的数据源（以及从它派生的`RowSet`对象）几乎总是关系数据库，但`CachedRowSet`对象能够从任何存储其数据的数据源获取数据。表格格式。例如，平面文件或电子表格可能是数据的来源。当实现断开的`RowSet`对象的`RowSetReader`对象以从这样的数据源读取数据时，这是真的。 `CachedRowSet`接口的参考实现具有`RowSetReader`对象，该对象从关系数据库读取数据，因此在本教程中，数据源始终是数据库。

涵盖以下主题：

*   [设置 CachedRowSet 对象](#setting-up-cachedrowset-object)
*   [填充 CachedRowSet 对象](#populating-cachedrowset-object)
*   [读者会做什么](#reader)
*   [更新 CachedRowSet 对象](#updating-cachedrowset-object)
*   [正在更新数据源](#updating-data-source)
*   [作家是什么](#writer)
*   [通知听众](#notifying-listeners)
*   [发送大量数据](#sending-large-amounts-of-data)

设置`CachedRowSet`对象涉及以下内容：

*   [创建 CachedRowSet 对象](#creating-cachedrowset-object)
*   [设置 CachedRowSet 属性](#setting-cachedrowset-properties)
*   [设置关键列](#setting-key-columns)

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

*   [使用默认构造函数](#using-default-constructor)
*   使用从`RowSetProvider`类创建的`RowSetFactory`实例：有关详细信息，请参阅[使用](jdbcrowset.html#rowsetfactory)[中的 RowSetFactory 接口](jdbcrowset.html)使用 JdbcRowSet 对象。

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

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

```
CachedRowSet crs = new CachedRowSetImpl();

```

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

`SyncProvider`对象提供`RowSetReader`对象（_ 读取器 _）和`RowSetWriter`对象（_ 写入器 _），断开的`RowSet`对象需要这些对象才能读取来自其数据源的数据或将数据写回其数据源。读者和作者的作用将在后面的[部分](#reader)和[什么作者](#writer)中解释。要记住的一件事是读者和作者完全在后台工作，所以解释他们的工作方式仅仅是为了您的信息。在读者和作者身上有一些背景知识可以帮助你理解`CachedRowSet`界面中定义的一些方法在后台做什么。

通常，属性的默认值可以正常，但您可以通过调用适当的 setter 方法来更改属性的值。有些属性没有默认值，您必须自己设置。

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

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

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

以下代码行设置`username`，`password`和`url`属性，以便可以使用`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 对象](#syncresolver)部分中看到如何以及何时使用键列。

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

```
crs.execute();

```

`crs`中的数据是通过在 command 属性中执行查询而生成的`ResultSet`对象中的数据。

不同的是`execute`方法的`CachedRowSet`实现比`JdbcRowSet`实现要多得多。或者更确切地说，方法执行委托其任务的`CachedRowSet`对象的读取器做了很多。

每个断开连接的`RowSet`对象都有一个`SyncProvider`对象，该`SyncProvider`对象提供`RowSet`对象的 _ 阅读器 _（`RowSetReader`对象）。创建`crs`对象时，它被用作默认的`CachedRowSetImpl`构造函数，除了设置属性的默认值外，还将`RIOptimisticProvider`实现的实例指定为默认的`SyncProvider`对象。

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

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

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

本节包括以下主题：

*   [更新列值](#updating-column-value)
*   [插入和删除行](#inserting-and-deleting-rows)

更新`CachedRowSet`对象中的数据与更新`JdbcRowSet`对象中的数据相同。例如，来自 `[CachedRowSetSample.java](gettingstarted.html)`的以下代码片段将`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](gettingstarted.html)`将新行插入`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`对象连接到其数据源，因此方法`updateRow`，`insertRow`和`deleteRow`可以更新`JdbcRowSet`对象和数据源。但是，在断开`RowSet`对象的情况下，这些方法会更新存储在`CachedRowSet`对象内存中的数据，但不会影响数据源。断开连接的`RowSet`对象必须调用方法`acceptChanges`才能将其更改保存到数据源。在库存场景中，返回总部，应用程序将调用方法`acceptChanges`以使用列`QUAN`的新值更新数据库。

```
crs.acceptChanges();

```

与方法`execute`一样，方法`acceptChanges`无形地完成其工作。方法`execute`将其工作委托给`RowSet`对象的读者，而方法`acceptChanges`将其任务委托给`RowSet`对象的编写者。在后台，编写器打开与数据库的连接，使用对`RowSet`对象所做的更改来更新数据库，然后关闭连接。

### 使用默认实现

困难在于可能出现 _ 冲突 _。冲突是指另一方更新了数据库中与`RowSet`对象中更新的值相对应的值的情况。哪个值应该在数据库中保留？作者在发生冲突时所做的工作取决于如何实现，并且有很多可能性。在频谱的一端，编写器甚至不检查冲突，只是将所有更改写入数据库。这是`RIXMLProvider`实现的情况，由`WebRowSet`对象使用。另一方面，编写器通过设置阻止其他人进行更改的数据库锁来确保不存在冲突。

`crs`对象的编写器是默认`SyncProvider`实现`RIOptimisticProvider`提供的编写器。 `RIOPtimisticProvider`实现的名称源于它使用乐观并发模型的事实。此模型假定冲突很少（如果有），因此不设置数据库锁。编写器检查是否存在任何冲突，如果没有冲突，则会将对`crs`对象所做的更改写入数据库，并且这些更改将变为持久性。如果存在任何冲突，则默认情况下不将新的`RowSet`值写入数据库。

在该方案中，默认行为非常有效。因为总部没有人可能会更改`COF_INVENTORY`的`QUAN`列中的值，所以不会发生冲突。结果，输入仓库`crs`对象的值将被写入数据库，因此将是持久的，这是期望的结果。

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

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

```
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`对象的该行中查找包含冲突值的列，该值将是非空值。从`resolver`和`crs`对象中检索该列中的值后，您可以比较两者并确定要保持哪一个。最后，代码使用方法`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);
                }
            }
        }
    }
}

```

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

本节包括以下主题：

*   [设置听众](#setting-up-listeners)
*   [通知如何运作](#how-notification-works)

`RowSet`对象的 _ 侦听器 _ 是从`RowSetListener`接口实现以下方法的组件：

*   `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`方法`cursorMoved`，`rowChanged`和`rowSetChanged`。 `cursorMoved`方法的实现可能是什么都不做，因为光标的位置不会影响`priceList`对象。另一方面，`rowChanged`和`rowSetChanged`方法的实现必须确定已经进行了哪些更改并相应地更新`priceList`。

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

示例代码 [`CachedRowSetSample.testCachedRowSet`](gettingstarted.html) 演示了如何以较小的片段发送数据。