本教程是针对JDK 8编写的。本页中描述的示例和实践未利用后续版本中引入的改进,并且可能使用不再可用的技术。
有关Java SE 9及后续版本中更新的语言特性的摘要,请参阅Java语言更改。
有关所有JDK版本的新功能、增强功能和已删除或弃用选项的信息,请参阅JDK发行说明。
CachedRowSet
对象是特殊的,因为它可以在不连接到数据源的情况下操作,也就是说,它是一个断开连接的RowSet
对象。它得名于它将数据存储在内存中(缓存),因此它可以操作自己的数据而不是数据库中存储的数据。
CachedRowSet
接口是所有断开连接的RowSet
对象的超级接口,因此这里演示的所有内容也适用于WebRowSet
、JoinRowSet
和FilteredRowSet
对象。
请注意,尽管CachedRowSet
对象(以及从它派生的RowSet
对象)的数据源几乎总是关系型数据库,但CachedRowSet
对象能够从以表格格式存储其数据的任何数据源获取数据。例如,一个平面文件或电子表格可以是数据的来源。当实现断开连接的RowSet
对象的RowSetReader
对象从这样的数据源读取数据时,这就成为了真实情况。CachedRowSet
接口有一个从关系型数据库读取数据的RowSetReader
对象,所以在本教程中,数据源总是数据库。
以下主题将被涵盖:
设置CachedRowSet
对象涉及以下操作:
使用RowSetProvider
类创建的RowSetFactory
实例来创建一个新的CachedRowSet
对象。
以下来自CachedRowSetSample.java
的示例创建了一个CachedRowSet
对象:
RowSetFactory factory = RowSetProvider.newFactory(); CachedRowSet crs = factory.createCachedRowSet();
对象crs
的属性默认值与创建JdbcRowSet
对象时的默认值相同。此外,它还被赋予了默认的SyncProvider
实现RIOptimisticProvider
的实例。
SyncProvider
对象提供了一个RowSetReader
对象(一个读取器)和一个RowSetWriter
对象(一个写入器),一个断开连接的RowSet
对象需要这两个对象来从数据源读取数据或将数据写回数据源。读取器和写入器的功能将在后面的章节读取器的功能和写入器的功能中解释。需要记住的一点是,读取器和写入器完全在后台工作,因此对它们的工作原理的解释仅供您参考。了解读取器和写入器的背景应该有助于您理解CachedRowSet
接口中的一些方法在后台的工作原理。
通常情况下,属性的默认值是可以的,但您可以通过调用相应的setter方法来更改属性的值。有一些没有默认值的属性必须由您自己设置。
为了获取数据,断开连接的RowSet
对象必须能够连接到数据源并具有一些选择要保存的数据的方法。以下属性保存获取数据库连接所需的信息。
username
:用户作为访问数据库的一部分提供给数据库的名称password
:用户的数据库密码url
:用户想要连接到的数据库的JDBC URLdatasourceName
:用于检索已在JNDI命名服务中注册的DataSource对象的名称您必须设置这些属性中的哪些取决于您如何进行连接。首选的方式是使用DataSource
对象,但您可能无法注册DataSource
对象到JNDI命名服务,这通常由系统管理员完成。因此,代码示例都使用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对象部分中看到关键列是如何以及何时使用的。
填充一个断开连接的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
对象中。最后,读取器关闭连接。
在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
对象连接到其数据源,所以方法updateRow
、insertRow
和deleteRow
可以同时更新JdbcRowSet
对象和数据源。然而,在断开连接的RowSet
对象的情况下,这些方法只能更新存储在CachedRowSet
对象内存中的数据,而不能影响数据源。断开连接的RowSet
对象必须调用方法acceptChanges
才能将其更改保存到数据源中。在库存场景中,总部的应用程序将调用方法acceptChanges
,以更新数据库中QUAN
列的新值。
crs.acceptChanges();
和execute
方法一样,acceptChanges
方法是在后台完成工作的。不同的是,execute
方法将工作委托给了RowSet
对象的reader,而acceptChanges
方法将工作委托给了RowSet
对象的writer。在后台,writer会打开与数据库的连接,将对RowSet
对象所做的更改更新到数据库中,然后关闭连接。
困难在于可能会出现冲突。冲突是指在数据库中有另一个方在对应于RowSet
对象中的值进行更新的情况。在数据库中应该保留哪个值?当发生冲突时,writer的处理方式取决于其实现方式,有很多可能性。在某些情况下,writer甚至不会检查冲突,只会将所有更改写入数据库。这就是RIXMLProvider
实现的情况,它是由WebRowSet
对象使用的。在另一种情况下,writer通过设置数据库锁来确保没有冲突,阻止其他人进行更改。
crs
对象的writer是默认的SyncProvider
实现RIOptimisticProvider
。RIOPtimisticProvider
实现得名于它使用的乐观并发模型。该模型假设很少或根本没有冲突,因此不会设置数据库锁。writer会检查是否有任何冲突,如果没有冲突,则将对crs
对象的更改写入数据库,这些更改将变为持久化。如果有任何冲突,默认情况是不将新的RowSet
值写入数据库。
在这种情况下,默认行为非常好用。因为总部不太可能更改COF_INVENTORY
的QUAN
列的值,所以不会发生冲突。因此,在仓库中输入到crs
对象的值将被写入数据库,并且成为持久化的,这是期望的结果。
然而,在其他情况下,可能会存在冲突。为了处理这些情况,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的值。从resolver
和crs
对象中检索该列中的值后,可以比较这两个值并决定哪个值要持久化。最后,代码使用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
接口中以下方法的组件:
cursorMoved
:定义当RowSet
对象中的游标移动时,监听器将执行的操作,如果有的话。rowChanged
:定义当一行或多行中的一个或多个列值发生变化,插入一行或删除一行时,监听器将执行的操作,如果有的话。rowSetChanged
:定义当RowSet
对象被填充新数据时,监听器将执行的操作,如果有的话。一个可能想成为监听器的组件示例是将RowSet
对象中的数据绘制为柱状图的BarGraph
对象。随着数据的变化,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.testPaging
演示了如何将数据分成较小的部分发送。