Java 6 RowSet 使用完全剖析

Java 6 RowSet 使用完全剖析



级别: 中级

徐 睿智     刘 威      许 彬 

来源网址: http://www.ibm.com/developerworks/cn/java/j-lo-java6rowset/

2008 年 6 月 05 日

C# 提供了 DataSet,可以将数据源中的数据读取到内存中,进行离线操作,然后再同步到数据源。同样,在 Java 中也提供了类似的实现,即 RowSet。 javax.sql.rowset 包下,定义了五个不同的 RowSet 接口,供不同的场合使用。本文将分别对这五个 RowSet 的使用场合以及详尽用法进行介绍,并且描述使用中可能出现的问题,以提醒读者在实际使用时绕开这些问题。

RowSet 简介

javax.sql.rowset 自 JDK 1.4 引入,从 JDK 5.0 开始提供了参考实现。它主要包括 CachedRowSet,WebRowSet,FilteredRowSet,JoinRowSet 和 JdbcRowSet。 除了 JdbcRowSet 依然保持着与数据源的连接之外,其余四个都是 Disconnected RowSet。

相比较 java.sql.ResultSet 而言,RowSet 的离线操作能够有效的利用计算机越来越充足的内存,减轻数据库服务器的负担,由于数据操作都是在内存中进行然后批量提交到数据源,灵活性和性能都有了很大的提高。RowSet 默认是一个可滚动,可更新,可序列化的结果集,而且它作为 JavaBeans,可以方便地在网络间传输,用于两端的数据同步。





回页首


类继承结构

RowSet 继承自 ResultSet,其他五个 RowSet 接口均继承自 RowSet。下图是它们的继承关系。


图 1. 继承结构图
图 1. 继承结构图 

表 1. RowSet 接口说明
CachedRowSet 最常用的一种 RowSet。其他三种 RowSet(WebRowSet,FilteredRowSet,JoinRowSet)都是直接或间接继承于它并进行了扩展。它提供了对数据库的离线操作,可以将数据读取到内存中进行增删改查,再同步到数据源。可串行化,可作为 JavaBeans 在网络间传输。支持事件监听,分页等特性。
WebRowSet 继承自 CachedRowSet,并可以将 WebRowSet 写到 XML 文件中,也可以用符合规范的 XML 文件来填充 WebRowSet。
FilteredRowSet 通过设置 Predicate(在 javax.sql.rowset 包中),提供数据过滤的功能。可以根据不同的条件对 RowSet 中的数据进行筛选和过滤。
JoinRowSet 提供类似 SQL JOIN 的功能,将不同的 RowSet 中的数据组合起来。目前在 Java 6 中只支持内联(Inner Join)。
JdbcRowSet 对 ResultSet 的一个封装,使其能够作为 JavaBeans 被使用,是唯一一个保持数据库连接的 RowSet。




回页首


实验环境

本文示例的实验环境如下:

  • Java 环境:Sun JDK 6.0
  • 数据库:derby-10.3.1.4
  • 数据库名:TESTDB
  • 数据库用户名及密码:均使用 derby 默认用户名和密码。
  • 表及测试数据:创建两个表:CUSTOMERS 和 ORDERS,并分别插入测试数据。
  • 示例代码以附件形式提供 下载

表 2. 表 CUSTOMERS
ID NAME REMARK
1 Tom Tom is VIP
2 Jim null

表 3. 表 ORDERS
ID USER_ID PRODUCT
1 1 Book
2 1 Computer
3 2 Phone




回页首


使用 CachedRowSet

填充 CachedRowSet 的两种方式

CachedRowSet 提供了两个用来获取数据的方法,一个是 execute(),另一个是 populate(ResultSet)

使用 execute() 填充 CachedRowSet 时,需要设置数据库连接参数和查询命令 command,如下示例代码:


清单 1. 使用 execute()
                
cachedRS.setUrl(DBCreator.DERBY_URL);
cachedRS.setCommand(DBCreator.SQL_SELECT_CUSTOMERS);
// derby 默认用户名和密码都是 "APP",也可以不设置。
cachedRS.setUsername("APP"); //$NON-NLS-1$
cachedRS.setPassword("APP"); //$NON-NLS-1$
cachedRS.execute();

cachedRS 根据设置的 url、username、password 三个参数去创建一个数据库连接,然后执行查询命令 command,用结果集填充 cachedRS,最后关闭数据库连接。execute() 还可以直接接受一个已经打开的数据库连接,假设 conn 为一个已经打开的数据库连接,下段示例代码与上段代码结果一致:


清单 2. 使用 execute(Connection)
                
cachedRS.execute(conn);
cachedRS.setCommand(DBCreator.SQL_SELECT_CUSTOMERS);
cachedRS.execute();

填充 CachedRowSet 的第二个方法是使用 populate(ResultSet)


清单 3. 使用 populate(ResultSet)
                
ResultSet rs = stmt.executeQuery(DBCreator.SQL_SELECT_CUSTOMERS);
cachedRS.populate(rs);
rs.close();

CachedRowSet 本身也是继承于 ResultSet,因此,也可以用一个有数据的 CachedRowSet 来填充另一个 CachedRowSet。

更新、删除、插入数据

更新数据。先把游标(cursor)移到要更新的行,根据每列的类型调用对应的 updateXXX(index, updateValue),再调用 updateRow() 方法。此时,只是在内存中更新了该行,同步到数据库需要调用方法acceptChanges() 或 acceptChanges(Connection)。如果 CachedRowSet 中保存着原数据库连接信息,则可以调用 acceptChanges();否则,则应该传入可用的数据库连接或重新设置数据库连接参数。下段示例代码更新第一行的第二列。


清单 4. 更新
                
cachedRS.first();
cachedRS.updateString(2, "Hello"); //$NON-NLS-1$
cachedRS.updateRow();
cachedRS.acceptChanges();

删除数据。把游标移到要删除的行,调用 deleteRow(),再同步回数据库即可。


清单 5. 删除
                
cachedRS.last();
cachedRS.deleteRow();
cachedRS.acceptChanges();

在删除数据时,需要注意布尔值 showDeleted 这个属性的使用。CachedRowSet 提供了 getShowDeleted() 和 setShowDeleted(boolean value) 两个方法来读取和设置这个属性。showDeleted 是用来判断被标记为删除且尚未同步到数据库的行在 CachedRowSet 中是否可见。true 为可见,false 为不可见。默认值为 false。

插入数据。插入操作稍微比更新和删除复杂。先看下段示例代码。


清单 6. 新增
                
cachedRS.last();
cachedRS.moveToInsertRow();
cachedRS.updateInt(1, 3);
cachedRS.updateString(2, "Bob"); //$NON-NLS-1$
cachedRS.updateString(3, "A new user"); //$NON-NLS-1$
cachedRS.insertRow();
cachedRS.moveToCurrentRow();
cachedRS.acceptChanges();

新插入的行位于当前游标的下一行。本例中,先把游标移到最后一行,那么在新插入数据后,新插入的行就是最后一行了。在新插入行时,一定要先调用方法 moveToInsertRow(),然后调用 updateXXX() 设置各列值,再调用 insertRow(),最后再把游标移到当前行。注意一定要遵循这个步骤,否则将抛出异常。

冲突处理

当我们使用 CachedRowSet 更新数据库时,有可能因为内存中的数据过期而产生冲突。此时更新数据库的方法 acceptChanges() 会抛出 SyncProviderException,由此我们可以捕获产生冲突的原因并手动进行解决。


清单 7. 冲突
                
ResultSet rs = stmt.executeQuery(DBCreator.SQL_SELECT_CUSTOMERS);
CachedRowSet cachedRS = new CachedRowSetImpl();
cachedRS.populate(rs);
cachedRS.setUrl(DBCreator.DERBY_URL);

// 修改数据库中的数据
stmt.executeUpdate("UPDATE CUSTOMERS SET NAME = 'Terry' WHERE ID = 1");

// 在 CachedRowSet 中更新同一行
cachedRS.absolute(1);
cachedRS.updateString(3, "Tom is not VIP");
cachedRS.updateRow();

SyncResolver resolver = null;
try {
    cachedRS.acceptChanges();
} catch (SyncProviderException e) {
    resolver = e.getSyncResolver();
}

while (resolver.nextConflict()) {
    System.out.println(resolver.getStatus());
}

我们首先填充 cachedRS,然后在数据库中直接修改 ID 为 1 的行,将 NAME 字段设为 "Terry",同时用 cachedRS 修改 ID 为 1 的行的 REMARK 字段,最后使用 acceptChanges 跟数据库进行同步。此时 cachedRS 中记录的原始值与数据库中的值不一致,从而产生冲突,抛出 SyncProviderException,数据也会更新失败。接下来我们通过 SyncProviderException 得到 SyncResolver 实例并遍历了产生的所有冲突。

SyncResolver 继承了 RowSet 接口,我们可以像使用一般 RowSet 一样操作它。SyncResolver 的实例拥有与正在同步的 RowSet 相同的行数和列数。使用 nextConflict() 和 previousConflict() 可以遍历所有产生的冲突,getStatus() 可以获得冲突的类型。在 SyncResolve 中定义了四种类型,分别是:DELETE_ROW_CONFLICT,INSERT_ROW_CONFLICT,NO_ROW_CONFLICT,UPDATE_ROW_CONFLICT。上例中产生的是 UPDATE_ROW_CONFLICT。

注:目前 Sun JDK 对 SyncResolver 的支持非常有限,只实现了 SyncResolver 接口中定义的方法,调用从 RowSet 接口继承的方法都会抛出 UnsupportedOperationException 异常;getConflictValue() 返回都是 null。

事件监听

一个监听器需要实现 RowSetListener 接口。RowSetListener 支持三种事件监听:cursor moved、row changed 和 rowSet changed。假定 Listener 实现了 RowSetListener 接口,看示例代码。


清单 8. 注册事件监听器
                
Listener listener = new Listener();
cachedRS.addRowSetListener(listener);
updateOnRowSet(cachedRS);
cachedRS.removeRowSetListener(listener);

updateOnRowSet() 所做的操作就是将游标移到第一行,更新,再同步回数据库。在这个方法中,依次触发了 Listener 的三个事件。下表列出了 CachedRowSet 中会触发监听器的所有方法。


表 4. CachedRowSet 中会触发监听器的方法
  cursor moved row changed rowSet changed
absolute()    
relative()    
next()    
previous()    
first()    
last()    
beforeFirst()    
afterLast()    
updateRow()    
deleteRow()    
insertRow()    
undoDelete()    
undoUpdate()    
undoInsert()    
populate()    
acceptChanges()    
acceptChanges(Connection)    
execute()    
execute(Connection)    
nextPage()    
previousPage()    
restoreOriginal()    
release()    

事务

事务对于保证数据的一致性是非常重要的。CachedRowSet 专门提供了处理事务的接口,从而可以保证同步数据的原子性和一致性。CachedRowSet 默认是不使用事务的。


清单 9. 事务代码一
                
cachedRS.absolute(1);
// 第一列不能为 null,更新时将产生冲突
cachedRS.updateNull(1);
cachedRS.updateRow();

cachedRS.next();
cachedRS.updateString(2, "Terry");
cachedRS.updateRow();
try {
    cachedRS.acceptChanges(conn);
} catch (SyncProviderException e) {
    // expected
}

rs = stmt.executeQuery(DBCreator.SQL_SELECT_CUSTOMERS);
cachedRS = new CachedRowSetImpl();
cachedRS.populate(rs);
printRowSet(cachedRS);

我们更新了 cachedRS 的第一行和第二行,并将第一行第一列错误的设置为 null。由于 CachedRowSet 默认不使用事务,第一行没有更新,而第二行更新成功。我们也可以手动控制事务的作用范围。


清单 10. 事务代码二
                
cachedRS.absolute(1);
cachedRS.updateNull(1);
cachedRS.updateRow();

cachedRS.next();
cachedRS.updateString(2, "Terry");
cachedRS.updateRow();
conn.setAutoCommit(false);
try {
    cachedRS.acceptChanges(conn);
    cachedRS.commit();
} catch (SyncProviderException e) {
    // expected
    cachedRS.rollback(); 
}
conn.setAutoCommit(true);
rs = stmt.executeQuery(DBCreator.SQL_SELECT_CUSTOMERS);
cachedRS = new CachedRowSetImpl();
cachedRS.populate(rs);
printCachedRowSet(cachedRS);

与前面的例子不同的是,这里我们关闭了自动提交模式,并且在同步失败后回滚了事务,避免了数据不一致的情况。

需要注意的是,如果需要手动控制事务的范围,在调用 execute 或 acceptChanges 时必须传入 Connection,否则再调用 CachedRowSet 的 commit() 或 rollback() 会抛 NullPointerException。实际上 CachedRowSet 依然是通过在内部保存 Connection 的引用来实现事务操作的。

分页

由于 CachedRowSet 是将数据临时存储在内存中,因此对于许多 SQL 查询,会返回大量的数据。如果将整个结果集全部存储在内存中会占用大量的内存,有时甚至是不可行的。对此 CachedRowSet 提供了分批从 ResultSet 中获取数据的方式,这就是分页。应用程序可以简单的通过 setPageSize 设置一页中数据的最大行数。也就是说,如果页大小设置为 5,一次只会从数据源获取 5 条数据。下面的代码示范了如何进行简单分页操作。(分页部分代码默认 ORDERS 表中有 10 条数据)


清单 11. 分页代码一
                
ResultSet rs = stmt.executeQuery(DBCreator.SQL_SELECT_ORDERS);
CachedRowSet cachedRS = new CachedRowSetImpl();
// 设置页大小
cachedRS.setPageSize(4);
cachedRS.populate(rs, 1);

while (cachedRS.nextPage()) {
printRowSet(cachedRS);
}
        
while (cachedRS.previousPage()) {
   printRowSet(cachedRS);
}

可以看到只需要在 populate 之前使用 setPageSize 设置页的大小,就可以轻松实现分页了。每次调用 nextPage 或 previousPage 进行翻页后,行游标都会被自动移动到当前页第一行的前面,并且只能在当前页内移动。这样我们对每一页都可以像新的数据集一样进行遍历,非常方便。这里需要注意的是:

  1. 用来填充 CachedRowSet 的 ResultSet 必须是可滚动的(Scrollable)。
  2. populate 必须使用有两个参数的版本,否则无法进行分页。读者可以将 cachedRS.populate(rs, 1); 换成 cachedRS.populate(rs); 看看会有怎样的情况发生。
  3. ResultSet rs 必须在遍历完毕后才能关闭,否则翻页遍历时会抛 SQLException

我们注意到在使用分页遍历数据集时,nextPage() 是最先被调用的,也就是说页与行相似,最开始的页游标是指向第 0 页

分享到:
评论

你可能感兴趣的:(java,jdk,数据结构,sql,Derby)