This TechNote presents working Java code
for accessing stored data using PSE from Object Design, Inc.
Download the compressed source code for this example here.
In some ways, this example really isn't fair. It implements the standard
DELETE, INSERT, SELECT, and UPDATE operations that we all know from
SQL. But an object storage mechanism is different. It can do things that
are extremely difficult in a system based on rows and columns. This TechNote
example doesn't do much to illustrate that difference. Also, it contains
some code that you might not expect to have to write yourself if you have
a relational database background, which may make you wonder about all this.
What's the real difference between a relational and an object database?
Relational databases require you to convert any data you want to store
into tables consisting of rows and columns of some fundamental data types.
Interrelationships between rows in different tables have to be derived
by the database engine. But stop thinking about data living on disks for
a moment. Instead, think about data living as objects in memory. These
objects don't exist in isolation. They're interrelated by design. That's
one of the things that makes them so useful. Querying objects is a process
of traversing the lattice of references that exist between objects. This
structure of this lattice usually mimics the problem you're trying to solve,
and query traversals tend to be simple, straightforward, and very application
specific. Universal query languages like SQL just can't do what's required
here. Now, instead of thinking about objects in memory, think about putting
an entire object lattice on a disk, then retrieving, updating, and storing
the parts you need when you need them, while maintaining their object relationships.
This is what PSE does for you.
Here's a case where an object storage mechanism is much simpler than
a relational mechanism. Imagine you're a realtor, and you want data about
every house you represent. Your data about each house needs to include
not just the number of rooms in a house, but the dimensions and location
of each room in each house. But houses have different numbers of rooms.
If you want to build a traditional relational table for this, there are two approaches.
The simple-minded approach is to define a schema for a house as fields
to store the data for some hypothetical maximum number of rooms. This has
two drawbacks. First, lots of space is wasted because few houses will have
that hypothetical maximum number of rooms. Second, sooner or later, somebody
will come to you with a house that has more rooms than that. Another relational
solution is to define a schema for just the individual rooms. In the first
approach, each house has several rooms, some of which might not exist.
In the second approach, each room has a house, which must be rebuilt whenever
you want to go into it. Things go downhill from there.
Instead, define a House as an object. One of its attributes might be
a Vector of Room objects. Since Vectors can hold as many or as few objects
as they need, all Houses can be represented very simply and efficiently,
and in a way that doesn't require much imagination to understand. Because
the representation is closely allied to the things as they really exist,
writing the code to discover important facts about them is fairly easy.
None of this implies that relational databases are bad.
Many problems are solved quite well by tables
that are accessed by a universal query language,
and the code that maps relational schemas into objects
is often simple enough to be generated automatically by software tools.
The strengths of relational systems,
such as application and language independence,
should not be ignored when deciding which kind of storage mechanism to use.
In some situations, however, an object database is the weapon of choice.
How This Example Works
The overall strategy of the example is to obtain a root object
from the PSE object store, traverse it in Java code to find the objects
we're looking for, do our work on those objects, and then commit them back
to the object store. You'll see how to create object stores, create and
obtain root objects, and commit changes back to the object store in
PseServer (not a part of the PSE product,
by the way, but written for this TechNote).
The amount of code here is quite small.
Processing the retrieved objects is carried out
in PseManager
(also not a part of the PSE product).
This code traverses the object lattice and does the useful work.
When a retrieved object needs to use another object that is still on the disk,
PSE retrieves it transparently.
When objects are displayed in the user interface,
they are actually copies of the ones in the object store.
It's important not to hold on to objects in the store
after a transaction has been committed,
but the displayed objects may be around for a long time.
Making copies solves this problem.
Applications that don't need to hold objects after they're committed
don't have to do this.
PseApplet (also not part of the PSE product)
is instantiated as your user interface, but most of the work is carried
out by its superclass, DbApplet. This UI is
simple and adequate, but it won't win any design awards. The amount of
code devoted to UI was intentionally minimized in this TechNote.
Good Object Oriented Stuff
Because this example deals only with objects, instead of objects and
relational tables, we can use more object-oriented techniques to build
it. In particular, our Person objects are very flexible, and can be extended
in many ways as our needs change.
The functional goal is to find all Persons that match the attributes of a partially-specified Person.
If you've read the code for DbSelect(poObject) in PseManager,
you know that it organizes a search and returns a result,
which is a Vector of PsePersons in this case,
but could just as easily be a Vector of instances of YourFavoriteClass.
The only constraint is that YourFavoriteClass must implement the
PseObject interface (also not a part of the PSE product),
which specifies four messages:
copy() returns a new instance where
all attributes in the new instance are copies of those in this
instance. This is not the same as clone(), which returns a new
instance where all attributes refer to the same objects as those in the
original. Reread the previous two sentences very carefully. Some object-oriented
textbooks call clone() a shallow copy, while the copy()
described here is a deep copy. Shallow copying is cheap, fast, and
easy. Deep copying can be expensive, slow, and dangerous (note that hard
isn't necessarily in that list). Nevertheless, it is sometimes necessary,
as it is in our database example here. The dangerous part has to do with
attributes copying attributes copying attributes copying attributes, etc.
If you have a circular object lattice, and you're not careful, copy()
could waste your whole afternoon. We won't have that problem here.
isEmpty() determines whether this
instance has anything interesting in it anywhere, or if it is just an empty
shell of an object.
matches(poPseObject) determines
whether this instance's attributes look the same as those that
are given in poPseObject. This is not the same as equals(),
which is intended to tell you if all attributes look the same. If
any attributes in poPseObject are empty (as in isEmpty(), above),
matches(poObject) ignores them in its comparison.
transfer(poPseObject) copies (as
in copy(), above) all attributes that aren't empty (as in isEmpty(),
above) from poPseObject to this instance.
There are at least two ways to build these methods. We will call these
two approaches The Left Way and The Right Way. Here's what code might look
like for The Left Way:
This is not actually in the class PsePerson, which extends Person,
but it could be.
public boolean matches
(
PseObject poPseObject // This is the object
I'm comparing myself to
)
{
boolean tName, tAddress, tPhone;
PsePerson oPsePerson = (PsePerson)poPseObject;
tName = (0 == oPsePerson.getName().length())
?
true // zero length matches by default
:
oName.equals(oPsePerson.getName());
tAddress = (0 == oPsePerson.getAddress().length())
?
true
:
oAddress.equals(oPsePerson.getAddress());
tPhone = (0 == oPsePerson.getPhone().length())
?
true
:
oPhone.equals(oPsePerson.getPhone());
return tName && tAddress && tPhone;
}
Actually, the compiler wouldn't let you get away with this. This code
assumes that the attributes of Person are all Strings. If they were, this
code would work fine. Let's assume for the moment that this is the case,
and that we later decide we need more capability in the form of specialized
Name, Address, and Phone objects that do more than just hold characters.
What does this matches() method look like now? We could rewrite
it, based on our knowledge of the Name, Address, and Phone classes we built,
and invoke methods on all of their attributes, and possibly on their attributes'
attributes, ad nauseam. This gets really hard to maintain, because it relies
on PsePerson explicitly knowing everything about all of its attributes'
object structures, which is something we'd like to avoid. A much simpler
solution derives from an old maxim of object-oriented programming: "Don't
do it yourself if you can get somebody else to do it for you."
The Right Way looks something like this:
public boolean matches
(
PseObject poPseObject // This is the object
I'm comparing myself to
)
{
return (getClass() == poPseObject.getClass())//
If we're the same class
?
matchAttributes(poDbObject) // then match our attributes
:
false; //
else we don't match
}
protected boolean matchAttributes
(
PseObject poPseObject // This is the object I'm
comparing myself to
)
PsePerson oPsePerson = (PsePerson)poPseObject;
return true
&& ((PseObject)getName ()).matches(oPsePerson.getName ())
&& ((PseObject)getAddress()).matches(oPsePerson.getAddress())
&& ((PseObject)getPhone ()).matches(oPsePerson.getPhone ())
;
}
There are three interesting things about this code.
First, let's deal with that OO programming maxim. The Right Way assumes
that all of PsePerson's attributes are PseObjects, so they'll respond to
matches() also. Rather than digging through all of our attributes'
attributes, let's ask our attributes to do that for us. If their attributes
have attributes, they can do the same thing. Eventually, some simple objects
at the outer reaches of the object graph will actually make comparisons
and return values. In this example, those simple objects are called PseStrings
(also not part of the PSE product). If you don't yet fully understand object
programming, this code looks recursive to you. It isn't. It's kind
of like having several layers of Managers over a lot of Dilberts. The Dilberts
do the actual work; the Managers just pass along messages from their Managers
to their subordinates and coordinate the resulting work (better than Dilbert's
manager does, we hope).
Second, why have these two methods, matches() and matchAttributes()?
Because, before the comparison can be carried out, we have to establish
that it can be done. In this case, that means making sure that this
instance and poPseObject are of the same class. Then matchAttributes()
delegates the comparisons. If we make a subclass of PsePerson called XXXPerson,
overriding matchAttributes() in XXXPerson like this is all we
have to do.
protected boolean matchAttributes
(
PseObject poPseObject // This is the object I'm
comparing myself to
)
XXXPerson oXXXPerson = (XXXPerson)poPseObject;
return super.matchAttributes(poXXXObject)
&& oXXX1().matches(oXXXPerson.getXXX1())
&& oXXX2().matches(oXXXPerson.getXXX2())
;
}
It's possible to do this without matchAttributes() methods,
but then the structure of matches() is more complex.
The third interesting thing about this code has nothing to do with databases
or object-oriented programming, but with software engineering. Why is matchAttributes()
formatted in this funny way? And why does PsePerson's version of it start
with that slightly wasteful conjunction with true? Because then
it's harder to make maintenance errors when the class definitions change.
Any correct changes to these methods involve deleting or inserting entire
lines that look just like their neighbors except for the attribute names.
Some of the Details
There are many things you need to understand if you're writing a PSE application from scratch.
First, since PSE is free, there are some limitations on simultaneous access.
Object Design will be happy to sell you an upgrade that allows concurrency.
Next, look closely at the code in PseServer.
These methods handle some issues of thread management that PSE requires.
And when you get a root object from the object store using get(),
be sure to call put() when you're done with it.
PSE does not collect its own garbage.
When you're really finished with an object,
you need to call ObjectStore.destroy(thatObject).
This can be a non-trivial issue,
and there is considerable documentation devoted to it.
Building Your PSE Code
An unusual aspect of PSE is the postprocessor that must be run on your application's Java classfiles.
This postprocessor injects PSE code that ensures the integrity of your objects
as they go back and forth between disk and memory.
You must tell the postprocessor which classes will be put into the object store (persistence-capable),
which classes handle those stored objects (persistence-aware),
and which classes are present but not involved with the stored objects.
Sometimes standard Java classes must be recompiled locally in order to be postprocessed correctly.
This example contains three such classes.
One of these classes, Observable, had to be modified slightly to have the desired result.
Specifically, its instance variables had to be declared as transient,
because this application doesn't need to store information about dependent Observers.
Be sure to read the
documentation from Object Design carefully.
The code for this example contains a script that correctly builds the application,
assuming PSE's pse.zip and tools.zip files are in your CLASSPATH,
and the postprocessor is in your PATH.
Here is a complete list of the files that were used to build this example.
Compile and run them on your own machine, and connect to an appropriate database.
Succeeding examples will show you how to construct two- and three-tiered architectures,
based on the code shown here.
Standard Java classes that must be recompiled locally for postprocessing
Observer.java
Observable.java
Vector.java
Interfaces used in both examples
NamedObject.java is needed by objects displayed by a ListApplet.
ObjectWithUI.java is needed by objects that provide a GUI to a containing applet.
DbManager.java specifies the messages needed by the back end of a DbApplet.
Objects used in both examples
Person.java is the basic data object that is stored and retrieved in these examples.
Applets used in both examples
ObjectApplet.java provides a common interface for composable GUI applets.
PersonApplet.java is the basic GUI for a Person.
ListApplet.java is a generic GUI that displays a list and a detail view.
UpdateApplet.java is a generic GUI that provides "before" and "after" views.
AppletFrame.java is a generic wrapper for external windows.
DbApplet.java is a simple and generic front end for a database.
Interfaces used in this example
PseObject.java specifies the messages needed to query and update these objects.
Objects used in this example
PsePerson.java implements the messages specified in the PseObject interface.
PseServer.java handles the PSE object store at a low level.
PseManager.java organizes queries and updates on the stored objects.
Applets used in this example
PseApplet.java supplies a small amount of extra information to a DbApplet.
Files used to build this example
make.bat compiles the sources in the context of this directory structure.
pc.out specifies which classes are persistence-capable.
pa.out specifies which classes are persistence-aware.
cc.out specifies which classes are simply copied into the application.
pse.html specifies the applet and its arguments.
view.bat runs the applet in the appletviewer.
Download the compressed source code for this example here.
TN-JAVA-02-9704
- Related Reading:
Any sample code included above is provided for your use on an "AS IS" basis, under the Netscape License Agreement - Terms of Use