★ wanayoo — archive 1999 http://developer.iplanet.com/docs/technote/database/pse/index.htmlNouvelle recherche | Portail wanayoo
iPlanet

You are here:  Home > Developers > TechNotes > Java > Java TechNote
Java TechNote
 iPlanet Developers


Developer Program
  Membership
  One-to-One Support
  Newsgroups
  Member Services

Developer Publications
  View Source
  Developer News

Documentation
  Technical Manuals
  White Papers
  TechNotes
  Sample Code
  FAQs
  Books

Technologies
  Application Server   CSS
  DOM
  CORBA
  Directory & LDAP
  Dynamic HTML
  Java
  JavaScript
  Linux
  RDF
  Security
  SSJS
  XML
  XUL

Developer Downloads
  Tools & SDKs
  Patches

iPlanet Products
  Technical Resources










spacer
Using Java with PSE
Writing Database Applications in Java

By Steven T. Abell
Technology Evangelist, Netscape Communications

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

spacer spacer


                                                       
iPlanet International | Year 2000 | Site Map | Feedback
Products | Solutions | Support | Services | Download | About Us | Developer
© 2000 Sun-Netscape Alliance. All Rights Reserved  Privacy Policy