JDBM Tutorial - 1

In the first of a series of tutorials that will end in a working application, we look at a simple persistence mechanism, and a grid for displaying data. In this series of tutorials, we're going to be showing how to enhance a simple application from a basic GUI to a full web enabled client and server. Our example application is ToDoTasks, a simple work tracking program. Each task has a name, notes, a priority, a percentage completed and when completed, the date it was complete.

In the ToDoTasks application, this is all displayed in a TasksFrame which has a JTable control, with a separate JTextArea for the notes. Underneath the hood, a class which implements the Tasks interface manages a collection of Task instances and generates TasksEvents when changes are made to it. A TasksModel listens to these TasksEvents and makes the Tasks' contents available to be rendered by the JTable which is created in TasksFrame. Finally, orchestrating these classes is a Controller class which creates the Tasks, TasksModel and TasksFrame.


Before we move on to expanding ToDoTasks, we're going to look at two elements of the application; persistence and presentation.


JDBM - When you absolutely don't need a database. There are times when all you need for an application is a simple manageable way to persist objects and even an embedded SQL database like HSQLDB or Derby/JavaDB is just a little too much for what you need. It's for situations like this that JDBM exists. JDBM at its simplest lets you store objects with a 'long' value for a record id. There are no database queries, just a simple find method to pull back an object by its record id. Of course it's not easy to work with just these record ids; for example, when an application starts up, how will it know which record ids represent what data. To get around this, JDBM has the ability for you to create named objects, where a name maps to a record id, so you can look up and retrieve record ids for 'important' records in the JDBM store.


You can find ToDoTasks JDBM implementation in JDBMTasksImpl.java. To use JDBM, we need to create a JDBM RecordManager, which you can get from the RecordManagerFactory giving it a file name as a parameter which will serve as the base name for the two files JDBM creates;


RecordManager recman;
...
recman=RecordManagerFactory.createRecordManager("tasks");

Storing a new object is simply a matter of calling the RecordManager insert method which will return the record id for retrieving the stored object. Setting a name for this record is done by calling the RecordManager's setNamedObject method with a name and the record id you want associated with that name. For example, from the Tasks generateTaskId method:

Long generatedTaskId;
...
generatedTaskId=new Long(1000L);
recid=recman.insert(generatedTaskId);
recman.setNamedObject("nextid",recid);

This stores a Long and creates a name, "nextid" associated with the record id returned by the insert. To retrieve an object from the store if you have the record id to hand, then the RecordManager fetch method will retrieve it;

generatedTaskId=(Long)recman.fetch(recid);

If you don't have the record id to hand and it's a named object, you can get the record id with RecordManager's getNamedObject. This will return either the record id or 0 if the named object doesn't exist. You can use that attribute to determine whether you need to initialise the store. Back in Tasks generateTaskId we have an example of that:

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);
}

Updating a record is a call to RecordManager's update method, which takes the record id and an object and replaces whatever is there with that object. Note that you can put a completely different class in the update and JDBM won't complain, it just sees things as serializable objects. This is good if you are persisting classes with a common subclass or interface but can lead to ClassCastExceptions if you don't manage it carefully.
When you've completed your updates or persisting new records, you need to commit the changes, using the RecordManager's commit method which completes the JDBM transaction. You can also use the rollback method to roll back changes to the last commit, but there are caveats to using that which we'll get back to. Here's the rest of generateTaskId which updates the stored task id:

generatedTaskId++;
recman.update(recid,generatedTaskId);
recman.commit();

Now, you could use just this part of the JDBM API to manage a lot of persistence, but there are two classes you can use which make JDBM much more useful, HTree and BTree. The HTree gives JDBM a simple persistent hash tree into which you can put() and get() keyed values but lacks ordering or size semantics. The BTree is a more scalable and manageable tree, with ordering and size semantics and the ability to browse the tree. We use the BTree in the Tasks class to store the Task instances. To create a BTree, we call a static method, createInstance, in BTree, giving it a record manager and a Comparator instance to allow it to compare keys. JDBM comes with a set of Comparators for Long, String and ByteArray; as our key is a Long, we can use the LongComparator:

BTree tasktable;
...
tasktable=BTree.createInstance(recman,new LongComparator());

Now we have a BTree instance, we can use the setNamedObject() method to save a reference to the record; the BTree keeps a reference to its own record id in the JDBM store which we can get using the getRecid() method.

recman.setNamedObject("tasktable",tasktable.getRecid());

To retrieve an existing BTree, we can now look it up using getNamedObject(), but to recreate the BTree instance, we use the static load() method of BTree. load() takes the record manager and record id as parameters and returns a BTree instance.

long recid=recman.getNamedObject("tasktable");
...
tasktable=BTree.load(recman,recid);

Working with the BTree is simple; we can use its insert method to add records:

Task toDoTask=new Task();
...
tasktable.insert(l,toDoTask,false);

The boolean parameter at the end sets whether the insert should replace the record if the key already exists. Here, we are inserting a new record, so it's set to false. The insert() method actually returns an object, which would be the value of the existing key. To retrieve a key's value, we use the find() method; in DBMTaskImpl.java we find:

public Task getTask(Long id) throws IOException {
return (Task)tasktable.find(id);
}

To navigate around the tree, there is the TupleBrowser which we can obtain from the BTree using the browse() method which returns a TupleBrowser positioned at the start of the tree's keys. If you give browse() a key, it will position at just before that key in the tree, and if you give it a null as a key, it will position at the end; in JDBMTasksImpl.java we use this in the getTaskIds() which needs to return an array of all the keys in order, so we browse from the start;

TupleBrowser tupleBrowser=tasktable.browse();

Once we have a TupleBrowser, we need to create a Tuple to hold key/value pairs:

Tuple tuple=new Tuple();

The TupleBrowser takes this tuple as a parameter when we call the TupleBrowser getNext() or getPrevious() methods. Both methods return a boolean, true if a Tuple has been retrieved, false if not. So to iterate through all the key/value pairs we can call getNext() till it returns false:

while(tupleBrowser.getNext(tuple)) {
Long key=(Long)tuple.getKey();
...
}

With the TupleBrowser we can navigate around the tree easily. If you make structural changes to the tree though, the TupleBrowser will become inconsistent and you should get a fresh instance of it. As mentioned earlier, there's a catch using BTree and HTrees and JDBM rollback, namely that rollback invalidates BTrees and HTrees in memory so if you do rollback, you need to reload any tree instances.


JDBM Tutorial - 1/2