见山只是山,见水只是水
浩渺天涯 丝丝愁难禁 魂飘四海醉梦里 幽幽几事堪醒? 黄叶无风自落 球云不雨长阴 惆怅红颜如梦 觉来无处能寻

数据库和 MIDP,第 4 部分:过滤和遍历策略


作者:Eric Giguere
2004 年 6 月

在本系列文章的第 2 和第 3 部分,我们讨论在数据和字节数组之间相互映射的基本方式,从而在记录存储中存储它们,这些记录存储是通过记录管理系统(Record Management System,RMS)管理的。读写数据总是需要克服的第一个障碍,但是发现您所需要的数据也同样的重要,并且这样做的话,您需要能够浏览记录存储,以一种有用的方式排序记录,以及使用过滤器提取出想要的数据。本文探索执行这些任务的不同策略;第 5 部分将建立在这里所学知识的基础之上,并且向您展示如何搜索满足指定规则的记录。

记录 ID 并不是索引


为了读或写一条记录,您需要知道它的记录 ID。在第 1 部分中,您了解到记录 ID 是一个整数值,它唯一地标识在记录存储中的一条记录 - 并且它不是记录存储中的索引。这个差异具有某些重要的含义。

如果具有 N 个记录的存储被索引,那么每条记录都将具有一个索引,其范围是 0 到 N-1 或者 1 到 N,这取决于范围是从 0 还是从 1 开始的。每当一条记录被删除或者插入的时候,在存储中的位于后面的记录索引将会相应的改变。该范围将会收缩或者增长,但是保持连续。

与索引不同的是,在记录存储中,无论在一条记录之前插入或者去除多少其他记录,该记录 ID 并不改变。增加到记录存储中第一条记录将其记录 ID 赋值为 1,下一条记录 ID 为 2,等等。如果您删除一条记录,它的记录 ID 变为无效,并且任何访问该记录的企图都会抛出 InvalidRecordIDException。无效的记录 ID 不会保持连续。

由于它们唯一地标识记录,因此您可以通过将一个记录的 ID 存储为另一条记录中的数据值,从而使用记录 ID 将两个或者更多记录连接到一起。您还可以使用记录 ID 来同步数据和外部应用程序,正如您将在第 6 部分中所看到的。

记录 ID 的主要缺点是它们使记录存储遍历变得复杂化;您不能像在数组中那样,在一个索引集合中进行迭代。您必须使用两种遍历技术之一:强制或者枚举。

 

强制遍历


利用强制方法,您只是简单地逐个提取记录,从第一条记录开始,忽略无效的记录,一直继续直到您已经获取所有的记录:

...
RecordStore rs = ... // an open record store
int lastID = rs.getNextRecordID();
int numRecords = rs.getNumRecords();
int count = 0;

for( int id = 1; 
     id < lastID && count < numRecords;
     ++id ){
    try {
        byte[] data = rs.getRecord( id );
        ... // process the data
        ++count;
    }
    catch( InvalidRecordIDException e ){
        // just ignore and move to the next record
    }
    catch( RecordStoreException e ){
        // a more general error that should be handled
        // somehow
        break;
    }
}
...

该代码调用 getNextRecordID() 来发现将要增加到存储的下一条记录的 ID,并且将它用作可能的记录标识符的上界。该代码在每次读取到一个有效值时都增加计数器,因此一旦已经看到所有记录,就可以停止遍历。注意,在遍历期间,记录存储没有被锁定 —— 如果它对于您很重要的话,您将需要使用一个 synchronized 块来防止其他线程改变该记录。

强制方法易于理解,并且如果缺失记录很少的话,将会工作得很好,但是首选的方法是使用枚举。

 

枚举记录


并不是检查每个记录 ID 以查看哪个记录是有效的,这本质上是强制方法所做的,您可以使用 RecordEnumeration 接口,请求 RMS 为您返回有效记录 ID 的一个枚举。该接口并没有扩展标准的 java.util.Enumeration 接口,但是它以一种类似的方式工作。事实上,它实际上是一个更有能力的接口:您可以向后或者向前遍历一个记录枚举,并且当记录发生改变时可以使用它来跟踪变化。

您可以针对希望遍历的记录存储调用 enumerateRecords() 方法,从而获取一个枚举,正如在该实例中:

...
RecordStore rs = ... // an open record store
RecordEnumeration enum = 
              rs.enumerateRecords( null, null, false );

... // use the enumeration here

enum.destroy(); // always clean it up!
...

出于简单性考虑,异常处理已经从该代码片断中被忽略了,但是应该意识到 enumerateRecords() 可以抛出一个 RecordStoreNotOpenException。

enumerateRecords() 的前面两个参数控制如何过滤和排序记录 —— 我们将简要地讨论它们。第三个参数控制枚举是否跟踪记录存储的变化。跟踪变化要求额外的耗费,并且在大多数情况下并不需要,因此我们将在所有的实例中将它设置为 false。

当您已经处理完一个枚举,您必须调用destroy() 方法,从而释放系统已经分配给它的任何资源 —— 记住,在 CLDC 中没有对象终止。未销毁的枚举将会导致您的应用程序泄漏内存。

使用 hasNextElement() and nextRecordId() 在枚举中向前移动:

...
RecordStore rs = ...
RecordEnumeration enum = ...

try {
    while( enum.hasNextElement() ){
        int id = enum.nextRecordId();
        byte[] data = rs.getRecord( id );
        ... // do something here
    }
}
catch( RecordStoreException e ){
    // handle the error here
}
...

您还可以使用 hasPreviousElement()previousRecordId() 向后移动。注意,如果一条记录从记录存储中被删除,同时该枚举是活动的,那么 nextRecordId()previousRecordId() 都将抛出 InvalidRecordIDException。

一个未排序枚举返回其记录的顺序是特定于实现,因此不要基于该顺序做出任何假设。

出于方便性考虑,您可以使用 nextRecord() 或者 previousRecord() 以获取后面或者前面记录的数据,而不是它的记录 ID:

...
try {
    while( enum.hasNextElement() ){
        byte[] data = enum.nextRecord();
        ... // do something here
    }
}
catch( RecordStoreException e ){
    // handle the error here
}
...

然而,您将不会知道正在讨论的记录 ID,或者不能直接将数据读取到您已经分配的字节数组中。然而,如果您正在过滤或者排序枚举,数据可能被缓存在内存中,因此从枚举中获取它可能比从底层的记录存储获取更为有效。

一个对于 numRecords() 的调用将向您提供枚举中的记录 ID,但是该调用将导致该枚举立即具体化而不是递增地具体化,并且该操作可能使用大量内存或者在处理中导致一个显而易见的停顿。

使用 reset() 将一个枚举重新设置为它的初始状态,使用 rebuild() 强制它根据记录存储的当前状态更新它自身。

使用 isKeptUpdated() 来检查一个枚举是否跟踪对于记录存储的变化,使用 keepUpdated() 以改变它的跟踪状态。

 

过滤枚举记录


只是对于在记录存储中的数据子集感兴趣吗?您可以通过使用一个过滤器,从而具有一个忽略不必要记录的枚举,该过滤器实现 RecordFilter 接口。过滤器的 matches() 方法应该指示是否一条记录将被包括在该枚举中:

public class MyFilter implements RecordFilter {
    public boolean matches( byte[] recordData ){
        ... // matching code here
    }
}

理想情况是,您需要过滤的信息是在数组的开头 —— 您希望尽可能快地确定一个匹配。为了使用一个过滤器,将它作为第一个参数传递给 enumerateRecords()

...
enum = rs.enumerateRecords( new MyFilter(), null, false );
...

记录 ID 并不传递给过滤器,只有字节数组包括记录数据。如果您必须知道正被匹配的是哪个记录 ID,您将需要在该记录中存储它自己的 ID,或者使用某些其他的方式从记录的数据中标识该记录。 例如,在第 3 部分中我们使用记录存储中的第一条记录,用于保持其余记录中的域的有关信息 —— 很明显,您希望将该记录从任何枚举中过滤出去。

 

排序枚举记录


为了保证记录以一种一致的、可预测的次序被返回,您必须排序该枚举。您向该枚举提供一个比较器,这是一个实现 RecordComparator 接口的对象。比较器的 compare() 方法返回一条记录是否先于、等于或者后于其他记录:

public class MyComparator implements RecordComparator {
    public int compare( byte[] r1, byte[] r2 ){
        int salary1 = getSalary( r1 );
        int salary2 = getSalary( r2 );

        if( salary1 < salary2 ){
            return PRECEDES;
        } else if( salary1 == salary2 ){
            return EQUIVALENT;
        } else {
            return FOLLOWS;
        }
        }

        private int getSalary( byte[] data ){
            ... // code to get salary info
        }
    }

注意常量 PRECEDESEQUIVALENT FOLLOWS 的使用。这些常量是通过 RecordComparator接口定义的。为了使用一个比较器,将它作为第二个参数传递给enumerateRecords():

...
enum = rs.enumerateRecords( null, new MyComparator(), false );
...

利用过滤器,没有记录标识符会被传递给比较器,只是原始的记录数据。

当您创建枚举时,可以提供过滤器和比较器。如果您这样做的话,在数据被排序之前应用过滤器。

 

下一步工作


现在,我们知道如何移动记录存储,以及如何排序和过滤记录,您已经准备好学习第 5 部分,该部分将描述相关策略,用于搜索一条记录存储中符合特定准则的对象。

 

关于作者


Eric Giguere 是 iAnywhere Solutions 的一名软件开发人员,iAnywhere Solutions 是 Sybase 的一个子公司,他在那里从事关于手持设备和无线计算的 Java 技术。他拥有 Waterloo 大学计算机科学的学士和硕士学位,并广泛撰写有关计算主题的文章。

posted on 2005-05-13 08:27  半边翅膀  阅读(472)  评论(0编辑  收藏  举报