在本系列教程的第一篇文章里,我们将通过一个实际的应用程序来讨论一个简单的保持机制和一个用于显示数据的网格。
我们的示例应用程序是ToDoTasks,这是一个简单的工作任务追踪程序。每项任务都一个名字、备注、优先顺序、完成的比例和何时完成,以及完成的日期。
在这个ToDoTasks应用程序里,所有信息都显示在一个TasksFrame里,它有一个JTable控件和一个单独的JTextArea用于存放备注。在这些表层内容之下,有一个用来实现Tasks界面的类会管理Task实例的集合,并在自己发生变化的时候生成TasksEvents。有一个TasksModel会侦听这些TasksEvents并让Tasks的内容由TasksFrame里创建的JTable来生成。最后,对这些类进行协调的是一个Controller类,它用来生成Tasks、TasksModel和TasksFrame。
本月,在开始深入剖析ToDoTasks之前,我们先来看这个应用程序中的两个要素:保持(persistence)和表现(presentation)。
JDBM——你绝对不需要数据库
有的时候,你对应用程序的要求只不过是用一种简单的方式来保持对象,甚至连诸如HSQLDB和Derby/JavaDB这样的嵌入式SQL数据库都已经大大超出了你的需要。就是在这样的情况下,JDBM才有了用武之地。JDBM让你轻松地用一个记录id的“long”值来保存对象。不需要数据库查询,只用一个简单的查找方法就能够通过记录id找到所需要的对象。当然,使用这些记录id并不是一件非常容易的事情;例如,在应用程序启动的时候,它怎么知道哪个记录id代表什么数据?为了解决这个问题,JDBM能够让你创建命名对象,也就是名字与记录id进行映射,这样你就可以查询和取回保存在JDBM里的“重要”记录的记录id了。
你可以在JDBMTasksImpl.java里找到ToDoTasks的JDBM实现。为了使用JDBM,我们需要创建一个JDBM RecordManager,如果你提供一个文件名作为参数,用作JDBM创建的两个文件的基础名,那么你就可以从RecordManagerFactory获得它:
RecordManager recman;
...
recman=RecordManagerFactory.createRecordManager("tasks");
保存新的对象只不过就是调用RecordManager插入方法,它会返回用于取回保存对象的记录id。通过调用RecordManager的setNamedObject方法以及与该名字相关的名字和记录id,就能够为这条记录设置名字。例如TasksgenerateTaskId方法:
Long generatedTaskId;
...
generatedTaskId=new Long(1000L);
recid=recman.insert(generatedTaskId);
recman.setNamedObject("nextid",recid);
它保存了一个Long并创建了一个名字“nextid”,它与插入所返回的记录id相关联。为了取回保存的对象,如果你手上有记录id,那么RecordManager方法就会把它取回来:
generatedTaskId=(Long)recman.fetch(recid);如果你手上没有记录id,而它是一个命名对象,那么你可以通过RecordManager的getNamedObject来获得记录id。这将返回记录id或者0,如果命名对象不存在的话。你可以使用这个数据来确定是否需要初始化保存的东西。回到TasksgenerateTaskId,我们就有了下面的例子:
Long generatedTaskId;
long recid=recman.getNamedObject("nextid");
if(recid==0) {
// There is no existing nextid; we create it
generatedTaskId=new Long(1000L);
recid=recman.insert(generatedTaskId);
recman.setNamedObject("nextid",recid);
} else {
// We have an existing nextid; get the record
generatedTaskId=(Long)recman.fetch(recid);
}
更新记录就是调用RecordManager的更新方法,它接受记录id和对象,并替换掉对象所带有的内容。要注意的是,你可以把一个完全不同的类放到更新里,而JDBM不会报错,因为它会把这些东西看作是可序列化的对象。如果你在用一个通用的子类或者接口来保持类,这非常理想,但是如果你不仔细管理的话,这会造成ClassCastExceptions异常。
在完成更新或者保持新记录之后,你需要通过使用RecordManager的commit方法(这个方法用来完成JDBM事件处理)来提交更改。你也可以使用rollback方法回滚到最后一次提交的地方,但是使用这个方法会有一些限制,我们稍后会来讨论这个问题。下面就是用来更新已保存任务id的generateTaskId的剩下的部分:
generatedTaskId++;
recman.update(recid,generatedTaskId);
recman.commit();
现在,你可以只使用JDBM API的这一部分来管理剩下的保持,但是有两个类你可以用来让JDBM更加有用——HTree和BTree。Htree会给JDBM一个简单的保持哈希树,你可以在其中put()和get()键值,但是它缺少排序和大小语义。BTree是一个可伸缩性和可管理性更强的树,具有排序和大小语义,并能够浏览该树。我们在Tasks类里使用BTree保存Task实例。要创建BTree,我们就要在BTree里调用一个静态方法——createInstance,赋予其一个记录管理器和一个Comparator实例,让其能够对键进行比较。JDBM带有一组用于Long、String和ByteArray的Comparator;由于我们的键是一个Long,所以我们可以使用LongComparator:
BTree tasktable;
...
tasktable=BTree.createInstance(recman,new LongComparator());
现在我们有了一个BTree实例,我们就可以使用setNamedObject()方法向记录保存参考了;BTree有一个参考,指向在JDBM里保存的自己的记录id,所以我们可以用getRecid()方法来获取:
recman.setNamedObject("tasktable",tasktable.getRecid());为了取回一个已有的BTree,我们现在使用getNamedObject()来查找它,但是为了创建BTree实例,我们使用了BTree的静态方法load()。load()将记录管理器和记录id作为其参数,并返回一个BTree实例。
long recid=recman.getNamedObject("tasktable");
...
tasktable=BTree.load(recman,recid);
使用BTree非常简单;我们可以使用其insert方法来添加数据:
Task toDoTask=new Task();
...
tasktable.insert(l,toDoTask,false);
结尾处的布尔参数用来设置如果键已经存在,insert是否应该替换掉记录。在这里,我们插入了一条新的记录,所以它被设置为“伪(false)。insert()方法事实上会返回一个对象,这是已经存在的键的值。要取回键的值,我们就要使用find()方法;在JDBMTaskImpl.java里,我们发现:
public Task getTask(Long id) throws IOException {
return (Task)tasktable.find(id);
}
要浏览这个树,我们可以使用通过browse()从BTree里获取的TupleBrowser,browse()方法返回一个TupleBrowser,放在树的键的开始部分。如果你向browse()提供了一个键,它就会位于树里的键的前面,如果你向它提供了一个“空(null)”作为键,那么它就会位于结尾;在JDBMTasksImpl.java中,我们在getTaskIds()里使用了它,getTaskIds()需要返回一个所有键按顺序排列的数组,所以我们就从开始进行浏览:
TupleBrowser tupleBrowser=tasktable.browse();一旦我们有了TupleBrowser,就需要创建一个Tuple来保存键/值对:
Tuple tuple=new Tuple();当我们调用TupleBrowsergetNext()或者getPrevious()方法的时候,TupleBrowser将tuple作为一个参数。这两个方法都会返回一个布尔值,如果Tuple被取回,那么返回的就是“真(true)”,如果没有被取回,那么返回的就是“伪(false)”。所以要在所有的键/值对里迭代,我们可以调用getNext()知道它返回伪值:
while(tupleBrowser.getNext(tuple)) {
Long key=(Long)tuple.getKey();
...
}
通过TupleBrowser,我们可以很容易在树里浏览。如果你对树进行结构性的调整,那么TupleBrowser将变得前后不一致,而且你应该碰到它的一个全新的实例。就像先前提到的,会有一个使用BTree和Htrees以及JDBM回滚的catch语句,也就是说内存里非法的Btrees和Htrees就会被回滚,如果你进行回滚,那么就需要重新加载所有的树实例。