So you're interested in object-oriented databases (OODBs), but you're still not quite sure how you'd use them. My previous article, "Object-Oriented Databases Are Worth a Closer Look", discussed the pros and cons of OODBs but didn't provide any practical examples of how the technology works. Luckily, a major benefit of many OODB solutions is the ability to start coding an application without immediate integration or prior knowledge of the OODB itself.
In this article, I'll walk you through the evolution of a simple application from a pure Java app to an OODB-enabled Java app. I'll demonstrate how to create the database on the fly and give you the source code for the whole solution so you can better understand how to tie an OODB into your apps.
First Things First
Before I get to the source code, let's get a few details out of the way:
Required Tools
First, I assume you have access to a fairly recent Unix or Windows OS. If you need to duplicate a Unix environment on your Windows machine, you could look into cygwin, although the Windows scripts I have provided run under Windows command prompt configurations (tested only on Windows 2000 and Win95). Table 1 lists all the required tools.
Table 1: Required Tools for OODB App
The Design of the System
For our demonstration, I will build a simple online phone book that manages phone entries, allowing query, insertion, update, and delete functionality. Since the migration from a database-independent design to an OODB design is fairly seamless, your system shouldn't be disrupted. The UML diagram shown in Figure 1 is a first stab at the design, before I planned for a specific OODB solution.
 |
|
Figure 1 The PhoneApp Design Before Employing an OODB
|
Because the application is such a simple one, the design is not overly elegant. The application runs from the Admin class and the data layer consists of just four classes: Phonebook, PhonebookEntry, PhoneRegion, and PhoneRegionList. All this information should be persistent and managed by some form of database management system. That's where our OODB will come in. (I skipped a business rules layer altogether to keep the demonstration simple.)
As you probably noticed in Table 1, I also decided to use an XML parser. The XMLPhoneEntries class manages parsing and retrieval of phone entries that have been stored in an XML file for batch upload. It uses Xerces-J, a very fast and popular XML parser. (Find more information on XML parsers in my previous DevX article "XML Parsers: DOM and SAX Put to the Test.")
Picking the OODB
Choosing an OODB for this demo was not trivial. No two OODBs work identically and they have major functional differences amongst them. Whereas most relational databases are fairly similar in their functionality, each OODB vendor implements its database product quite differently.
I ended up choosing ObjectStorePSE from ObjectDesign Inc.. ObjectStore is one of the more popular OODBs. It has a fairly compact installation and can be downloaded for a 30-day trial. However, ObjectStore certainly shared some of the OODB disadvantages I discussed in "Object-Oriented Databases Are Worth a Closer Look".
Building the Application
Now that we've got everything we need, let's get down to business. This section will walk you through the key aspects of building an OODB-enabled application. I am presuming that you already have set up J2SE, Xerces-J, ObjectStore PSE, and the various settings of the environment. You can use links to the full code at the end of the article.
Commence Coding
One of the major benefits of many OODB solutions is the ability to start coding the application without immediate integration or prior knowledge of the OODB itself. OODB implementations vary, but many allow classes to exist in the data layer without being aware of the OODB technology behind the scenes.
In order to show the evolution of the application from pure Java to an OODB-enabled Java application, I developed the code in two phases: database-independent and OODB-enabled. In the pre-OODB code, I simply implemented the design shown in Figure 1. The functionality was fairly simple: the addition of single entries or batch uploads of phone book entries from an XML file. The following code shows the execution of the application:
0: Quit, 1: Add, 2: Upload
Choice: 2
Enter new batch phone list...
XML File Name [batchEntries.xml]: batchEntries.xml
Added 555-123-4567: Bean, Ed 13 No Where Street (apt. 2a) [...]
Added 555-555-1212: Doe, John 1 No Such Street [...]
Without a persistence or serialization solution, added entries would be lost as soon as the program terminated. This application needs some form of persistence to go to the next step.
Setting Up the OODB
By simply adding an OODB, the functionality of this application was enhanced. At the very least, an OODB version should support basic query, delete, update, and improved reporting functionality.
I first had to decide where the OODB interface would reside. I didn't have to modify all data classes to know about the OODB, since an OODB simply stores objects when asked. However, some code had to tell the OODB which objects to store, retrieve, modify, etc. Looking at the design, the Phonebook class appeared to be the most likely candidate to enhance, given its container-like behavior. Whereas the first version of Phonebook.java relied on a stack for storage (a map probably would have been a better choice), the new version tied into the ObjectStore OODB for storage.
Other than setup, which takes place in the code, no database installation or setup is necessary. Unlike most relational databases, creation of the database in this demo is done on the fly. Let's look at the basics of an ObjectStore-enabled Phonebook class.
Code: Phonebook Import Statements
import com.odi.*;
import com.odi.util.*;
import com.odi.util.query.*;
import java.util.Map;
import java.util.Set;
import java.util.Iterator;
import java.util.Collection;
The ObjectStore packages are imported so the code can use them. Without including the query package, I would be unable to perform dynamic run-time queries against the database. The containers, iterators, and collections of the java.util hierarchy are required in order to interface with the OODB.
Code: Phonebook Database and Session Setup
public Phonebook() {
session = Session.create(null, null);
session.join();
try {
db = Database.open(database, ObjectStore.UPDATE);
} catch (DatabaseNotFoundException e) {
db=Database.create(database, ObjectStore.ALL_READ | ObjectStore.ALL_WRITE);
}
The above code ensures that a database is created and that a session to the ObjectStore PSE OODBMS exists. If the database does not already exist, it is created with read and write permissions.
Code: Phonebook Entry Point
Transaction t = Transaction.begin(ObjectStore.UPDATE);
try {
entries = (OSHashMap) db.getRoot("entries");
} catch (DatabaseRootNotFoundException e) {
db.createRoot("entries", entries = new OSHashMap());
}
t.commit(ObjectStore.RETAIN_READONLY);
}
The above code creates an "entry point" into the OODB. This is similar to defining a relational table name. In the case above, we test to see if the entry point "entries" exist. If not, we create a new OSHashMap with the name "entries," within which we will store our PhonebookEntry objects. The OSHashMap is basically a subclass of the java.util.Map with some added features and persistence capability.
Code: Phonebook Cleanup
public void close() {
db.close();
session.terminate();
}
It is important to properly terminate database and session resource usage before quitting the application. This method should be explicitly called because finalize() does not run predictably, even on shutdown. You can force finalize() to run, but I don't recommend it versus an explicit close() method.
Creating a Persistent Class
The Java code can save any object to the PSE OODB without modifying class contents. The exception is the Phonebook class, which provides the interface to the OODB. The PhoneRegion and PhoneEntry classes do not have to have any special code changes or additions before being stored in the OODB. They do, however, need to be post-processed by an ObjectStore tool in order to be prepared for storage in the database.
Code: Makefile Post-Processing of Stored Objects
osjcfp -dest ./pData -pc PhoneApp/Data/PhonebookEntry.class
In the above code, the command osjcfp (ObjectStore Java Class File Post-processor) takes Java class files and adds extra information about those classes to support the OODB. This command generates a number of files that must be stored in a separate directory (specified by -dest). The command must also know which class files to post-process. In this case we are post-processing the PhonebookEntry.class file.
Database Inserts
Database inserts imply that one or more objects are stored in the OODB. Insertion of objects must take place after a session and database connection has been established. When an object is inserted, all of its attributes (included contained objects) are stored with their proper type and value. This is demonstrated in the following piece of code:
Code: Phonebook Management of PhonebookEntry Inserts
public void addEntry(String firstName, String lastName, ...) {
PhonebookEntry entry = new PhonebookEntry( firstName, lastName, ... );
if ( entry.isValid() ) {
Transaction t = Transaction.begin(ObjectStore.UPDATE);
if (entries.get(phoneNumber) != null) {
t.abort(ObjectStore.RETAIN_READONLY);
System.out.println("Entry already exists.");
} else {
entries.put(phoneNumber, entry);
t.commit(ObjectStore.RETAIN_READONLY);
}
} else {
System.out.println("Invalid entry (" + entry.toString + ")");
}
}
A new PhonebookEntry object is created before being inserted. This object is then tested for validity (format, mandatory fields, etc). Before it is inserted, a transaction is started. If an object already exists in the database with the same "primary key" (phone number), then the insertion aborts. Otherwise, code puts the entry into the OSHashMap, "entries", named by its key, the phoneNumber for that entry. After a successful insertion, the transaction is committed and closed.
Performing Queries
In an OODB-enabled Java application, code queries a collection that corresponds to an entry point in the database. In the previous entry point discussion, I created an "entries" entry point with an OSHashMap. This collection resides within the OODB and stores our PhonebookEntry objects.
Java applications can perform two types of queries against OODB collections: constant and variable queries. In RDBMS terms, a variable query would refer to dynamic SQL. First, let's look at a constant query.
Code: Simple Queries in the Phonebook Class
Query query = new Query(
PhonebookEntry.class,
"firstName == \"John\""
);
Collection queryResults = query.select(entries.values());
String[] results = new String[queryResults.size()];
Iterator resultIterator = queryResults.iterator();
int count = 0;
while(resultIterator.hasNext()) {
PhonebookEntry tmpEntry = (PhonebookEntry)resultIterator.next();
results[count] = tmpEntry.toString();
count++;
}
In the above code (missing things like Transaction code to preserve space), a static query is coded into the application. It's looking for any PhonebookEntry objects with firstName equal to "John." The query runs against the values of the entries OSHashMap. Remember, the entries object actually refers to the "entries" entry point into the OODB. Also keep in mind that the query is running against the OODB, so objects are retrieved into the Java heap only when explicitly accessed.
Once the query is performed, the code then iterates through all results, retrieving the PhonebookEntry objects and calling their toString method to get a text summary of each entry.
Dynamic queries allow the code to specify fields and variables at runtime. The ObjectStore mechanism for this type of query uses FreeVariables and FreeVariableBindings. A FreeVariables object defines "place holders" in the query that will be assigned values at runtime. The FreeVariableBindings object assigns the values to the "place holders" at runtime. (This concept is described in much more detail in the ObjectStore PSE "Getting Started" tutorial included with the evaluation download.) The example below shows a sample query using variables:
Code: Dynamic Queries in the Phonebook Class
FreeVariables dynamicVariables = new FreeVariables();
dynamicVariables.put("firstNameValue", String.class);
Query query = new Query(
PhonebookEntry.class,
"firstName == firstNameValue",
dynamicVariables
);
FreeVariableBindings binding = new FreeVariableBindings();
binding.put("firstNameValue", newFirstName);
Collection queryResults = query.select( entries.values(), binding);
[Iteration through results same as in previous code snippet]
Performing Updates
Updates are very easy with an OODB. The object already exists in the database, so it is simply a matter of retrieving the object and then calling its mutator methods. You can also set the object's public attributes directly if you wish. Once you commit the transaction, the changes are applied transparently to the object in the database.
Code: Updating existing PhonebookEntry Objects
Transaction t = Transaction.begin(ObjectStore.UPDATE);
PhonebookEntry entry = (PhonebookEntry) entries.get("555-123-4567);
entry.setFirstName("Steve"); }
t.commit(ObjectStore.RETAIN_HOLLOW);
In the preceding example, the code retrieves an object from the hash map using a phone number as the key. More code is usually required to handle a null return due to a non-existent object. Once the object is retrieved, it can be manipulated as a normal Java object. Once all changes have been made to the object, the transaction is committed and changes are applied to the database.
See the Code in Full
By adding only 130 statement lines of code to the application, I was able to add query, update, delete, and reporting functionality with ObjectStore PSE. A brief transcript shows the new application in action.
At the beginning of this article, I mentioned that I started out writing an application with very little consideration of the final storage approach. You can view the final code in the following files:
Red sections are the pieces of code that I added while migrating from version 1 of the application to the OODB-enabled application. Some areas that I changed to OODB-enable the application are marked by comments (beginning with "Changed:" or "Added:") explaining the modifications.
You can download the source code for the application in case you would like to try building it for yourself. Be warned though, the code is partial and buggy, and it lacks error checking. (If you receive "Out of Environment Space" errors from older versions of Windows, refer to the README.TXT file.) THe code is intended only to show basic OODB conceptsreuse pieces at your own risk.
In a Nutshell
Our phone book app demonstrates some basic features of an ObjectStore PSE OODB. Each OODB is somewhat different of course, but many of the concepts in this article apply to other products. You can easily rebuild and tailor this demonstration to your own learning purposes. OODBs are not a silver bullet, and I hope I have not given that impression. They have some important limitations that you should be aware of (see "Object-Oriented Databases Are Worth a Closer Look").
In addition to this article, you also should study the whitepapers and documentation supplied with ObjectStore PSE. They address a number of general concepts and specific examples that were very useful when I first worked with ObjectStore PSE and in developing this article.
Steve Franklin handles the architecture and project engineering responsibilities at a major software firm dealing with J2EE, client/server, command and control, and other distributed architectures. Steve Franklin's primary "off-hours" hobby can be found at Lookoff.com, a repository for Internet and research resources. He can be reached at steve@lookoff.com.
|