ObjectStore Java Tutorial
Chapter 3

Writing Your Application to Use ObjectStore

This chapter discusses the core concepts involved with writing a ObjectStore application. It uses the Personalization application to provide examples of the concepts. This chapter discusses the following topics:

Getting Ready to Store Objects

Creating Database Entry Points

Storing Objects in a Database

Accessing Objects in the Database

Deleting Objects

Using Collections

Getting Ready to Store Objects

Before you can create and manipulate persistent objects with ObjectStore, you must perform the following operations:

Creating Sessions

To use ObjectStore, your application must create a session. A session is the context in which ObjectStore databases are created or opened, and transactions can be executed. Only one transaction at a time can exist in a session. In PSE and ObjectStore, you are limited to one session per Java VM process. PSE Pro allows you to create multiple sessions and thus have multiple concurrent transactions in a single Java VM process.

Any number of Java threads can participate in the same session. Each thread must join a session to be able to access and manipulate persistent objects. To create a session, you call the Session constructor and specify the host and properties. The method signature is

public static Session create(String host, 
      java.util.Properties properties)
A thread can join a session with a call to Session.join(). For example:

/* Create a session and join this thread to the new session. */
session = Session.create(null, null);
session.join();
ObjectStore ignores the first parameter in the create() method. You can specify null. The second parameter specifies null or a property list. See ObjectStore Java API User Guide, Description of Properties.

Creating, Opening, and Closing Databases

Before you begin creating persistent objects, you must create a database to hold the objects. In subsequent processes, you open the database to allow the process to read or modify the objects. To create a database, you call the static create() method on the Database class and specify the database name and an access mode. The method signature is

public static Database create(String name, int fileMode)
The initialize method in the UserManager class shows an example.

      public static void initialize(String dbName)
      {
      /* Other code, including creating a session and joining thread to session*/
            /* Open the database or create a new one if necessary. */
            try {
                  db = Database.open(dbName, ObjectStore.UPDATE);
            } catch (DatabaseNotFoundException e) {
                  db = Database.create(dbName, ObjectStore.ALL_READ | ObjectStore.ALL_WRITE);
            }
The initialize() operation first creates a session and then joins the current thread to that session. Next initialize() tries to open the database. If the database does not exist, DatabaseNotFoundException is thrown and is caught by initialize(), which then creates the database. initialize() also stores a reference to the database instance in the static variable db.

The Database.create() and the Database.open() methods are called with two parameters. In both methods, the first parameter specifies the pathname of a file. In the create() method, the second parameter is a UNIX-style protection number. In the open() method, the second parameter specifies the access mode for the database, that is, ObjectStore.UPDATE or ObjectStore.READONLY.

Shutting down

The UserManager.shutdown() method shows an example of how to close a database and how to terminate a session.

/** 
* Close the database and terminate the session.
*/
      public static void shutdown() {
            db.close();
            session.terminate();
      }

Starting Transactions

You create, destroy, open, and close a database outside a transaction. You access and manipulate objects in a database inside a transaction. Therefore, a program must start a transaction before it can manipulate persistent data. While the transaction is in progress, a program can read and update objects stored in the open database. The program can choose to commit or abort the transaction at any time.

Committing transactions

When a program commits a transaction, ObjectStore updates the database to contain the changes made to persistent data during the transaction. These changes are permanent and visible only after the transaction commits. If a transaction aborts, ObjectStore undoes (rolls back) any changes to persistent data made during that transaction.

Purpose of transactions

In summary, transactions do two things:

Creating transactions

To create a transaction, insert calls to mark the beginning and end of the transaction. To start a transaction, call the begin() method on the Transaction class. This returns an instance of Transaction and you can assign it to a variable. The method signature is

public static Transaction begin(int type)
The type of the transaction can be ObjectStore.READONLY or ObjectStore.UPDATE. Other transaction types are discussed in ObjectStore Java API User Guide, Description of Transaction Types.

Ending transactions

ObjectStore provides the Transaction.commit() method for successfully ending a transaction. When transactions terminate successfully, they commit, and their changes to persistent objects are saved in the database. The Transaction.abort() method is used to unsuccessfully end a transaction. When transactions terminate unsuccessfully, they abort, and their changes to persistent objects are discarded.

When an application commits a transaction, ObjectStore saves and commits any changes in the database. It also checks to see if there are any transient objects that are referred to by persistent objects. If there are, and if the referred-to objects are persistence-capable objects, ObjectStore stores the referred-to objects in the database. This is the process of transitive persistence, also called persistence by reachability.

The default commit operation makes all persistent objects inaccessible outside the transaction's context. After you commit a transaction, if you want to access data in the database, you must start another transaction and navigate to the object again from a database entry point. There are optional commit modes that allow you to retain the objects so that you can access them outside a transaction or in a different transaction.

Creating Database Entry Points

To access objects in a database, you need a mechanism for referring to these objects. In other words, you need an entry point. In a relational database system, the entry points are the tables defined to the database. The tables have names that you can use in queries to gain access to the rows of data. You cannot directly access a row by its table name.

In ObjectStore, the names or entry points are called roots and they are more flexible than in the relational database model.

Description of Database Roots

You can use a database root to name any object defined in the database. You can use a root to reference a collection object, which is ObjectStore's equivalent of a table. But you can also choose to assign roots to individual objects.

A database root provides a way to give an object a persistent name. A root allows an object to serve as an initial entry point into persistent storage. When an object has a persistent name, any process can look it up by that name to retrieve it. After you retrieve one object, you can retrieve any object related to it by navigating object references, or by a query.

Each database typically has a relatively small number of entry point objects, each of which allows access to a large network or collection of related objects.

Creating Database Roots

You must create a database root inside a transaction. You call the Database.createRoot() method on the database in which you want to create the root. The method signature for this instance method on the Database class is

public void createRoot(String name, Object object)
The name you specify for the root must be unique in the database. The object that you specify to be referred to by the root can be transient and persistence-capable, persistent, or null. If it is not yet persistent, ObjectStore makes it persistent automatically when you call createRoot().

Example of Creating Database Roots

In the remainder of the UserManager.intialize() operation, the Personalization application begins a transaction, and looks for the database roots allUsers and allInterests. If they are not there, the application creates them and then commits the transaction.

      public static void initialize(String dbName)
      //  database open code omitted
            /* Find the allUsers and allInterests roots or create them if not there. */
            Transaction tr = Transaction.begin(ObjectStore.UPDATE);
            try {
                  allUsers = (Map) db.getRoot("allUsers");
                  allInterests = (Set) db.getRoot("allInterests");
            } catch (DatabaseRootNotFoundException e) {
                  /* Create the database roots and give them appropriate values */
                  db.createRoot("allUsers", allUsers = new OSHashMap());
                  db.createRoot("allInterests", allInterests = new OSHashSet());
            }
            /* End the transaction and retain a handle to allUsers and allInterests */
            tr.commit(ObjectStore.RETAIN_HOLLOW);
      
Most of the methods defined on UserManager access the root objects, and use them to find a particular user, add a new user, or remove a user. The next section discusses these operations.

The Personalization application keeps track of all the users who register with the site, as well as the interests of each registered user. To track users, the application uses a persistent Map that is indexed on the user names. This allows quick look-up of a user in the database. To track interests, the application uses a Set of interests. These Maps and Sets are implementations of the JDK 1.2 collections. For more information, see Using Collections.

Storing Objects in a Database

Objects become persistent when they are referenced by other persistent objects. The application defines persistent roots and when it commits a transaction, ObjectStore finds all objects reachable from persistent roots and stores them in the database. This is called persistence by reachability and it helps to preserve the automatic storage management semantics of Java.

Example of Storing Objects in a Database

For example, in the Personalization application, consider the subscribeNewUser() method, which adds a new user to the database.

public static int subscribe(String name, String email)
            throws PersonalizationException
      {
            Transaction tr = Transaction.begin(ObjectStore.UPDATE);
            /* First check to see if the user's name is already there. */
            if (allUsers.get(name) != null) {
                  tr.abort(ObjectStore.RETAIN_HOLLOW);
                  throw new PersonalizationException("User already there: " + name);
            }
            /* The user name is not there so add the new user;
                   first generate a PIN in the range 0..10000. */
            int pin = pinGenerator.nextInt() % 10000;
            if (pin < 0) pin = pin * -1;
            User newUser = new User(name, email, pin);
            allUsers.put(name, newUser);
            tr.commit(ObjectStore.RETAIN_HOLLOW);
            return pin;
      }
The application checks whether the user name is already defined in the database. If the name is defined, ObjectStore throws PersonalizationException. If the name is not already defined, the application creates a new user, adds that user to the allUsers collection, and commits the transaction. Since the allUsers collection is already stored in the database, ObjectStore stores the new user object in the database when it commits the transaction.

In the Personalization application, another example of storing objects in a database is the addInterest() method defined on the User class. To add an interest to a user's set of interests, the application calls

interests.put(interestName, interest);
This adds an Interest object to the user's interests, which are stored in a Map. When the transaction commits, since the user is a persistent object and its Map is persistent, ObjectStore makes the new Interest object persistent. See Appendix A, Source Code for the complete code.

Definition of Persistence-Capable

An object must be persistence-capable for an application to be able to store that object in an ObjectStore database. Persistence-capable is the capacity to be stored in a database. If you can store an object in a database, the object is persistence-capable. If you can store the instances of a class in a database, the class is a persistence-capable class.

(ObjectStore also allows for classes that are persistence-aware. Persistence-aware classes can access and manipulate instances of persistence-capable classes, but cannot themselves be stored in a database. See ObjectStore Java API User Guide, Creating Persistence-Aware Classes.)

To make a class persistence-capable, you compile the class definitions as usual and then run the ObjectStore class file postprocessor on the class files. The class file postprocessor annotates the classes you define so that they are persistence-capable. This means that the postprocessor makes a copy of your class files, places them in a directory you specify, and adds lines of code (annotations) that are required for persistence. Details about how to run the postprocessor are in Chapter 4, Compiling and Running an ObjectStore Program.

The annotations required by ObjectStore and added by the postprocessor allow ObjectStore to understand the representation (state) of objects so that it can save the state to persistent storage. The annotations also allow ObjectStore to automatically ensure that

Accessing Objects in the Database

After an application stores objects in a database, the application can use references to these objects in the same way that it uses references to transient objects. An application obtains initial access to objects in a database through navigation from a root or through an associative query. An application can retain references to persistent objects between transactions to avoid having to obtain a root at the start of each transaction.

Example of Using a Database Root

To access objects in a database, you must start a session, open the database, and start a transaction. Then you can obtain a database root to start navigating the database. For example, in the Personalization application, you obtain the "allUsers" root to obtain User objects.

allUsers = (Map) db.getRoot("allUsers");

Example of Using References

Consider again the subscribe() method in the Personalization application. The first part of this method protects against storing a duplicate name by checking whether the user's name is already in the database. For example:

      /* First check to see if the user's name is already there. */
            if (allUsers.get(name) != null) {
                  tr.abort(ObjectStore.RETAIN_HOLLOW);
                  throw new PersonalizationException("User already there: " + name);
            }
Since the class variable allUsers references the allUsers collection, the application can use the standard Java Map.get() method to check if the name is already stored. The same code would work for a persistent or transient collection.

Retaining Objects or References to Objects

Each time the Personalization application commits a transaction, it specifies the ObjectStore.RETAIN_HOLLOW option. This option keeps references that were available during the transaction. The application can use the references in subsequent transactions.

After the Personalization application commits the initial transaction, the class variable allUsers continues to reference the allUsers collection. When it begins a new transaction, the application does not need to reestablish a reference to allUsers with the getRoot() method.

When an application commits a transaction, the default retain option is ObjectStore.RETAIN_STALE. This option makes all persistent objects inaccessible outside a transaction. To access any objects in the database, you must start a transaction, use a root to access an initial object, and navigate to other objects from the root. For example, if the Personalization application specifies ObjectStore.RETAIN_STALE when it commits a transaction, it cannot access the allUsers collection outside a transaction. Also, to access the allUsers collection again, the application must start a new transaction and obtain a new reference with a call to the getRoot() method to obtain the allUsers Map.

If you want to access the contents of persistent objects outside a transaction, you can specify the ObjectStore.RETAIN_READONLY or ObjectStore.RETAIN_UPDATE option. These options allow you to read or update objects whose contents were available during the transaction. For example, the Personalization application specifies the ObjectStore.RETAIN_READONLY option in validateUser().

public static User validateUser(String userName, int PIN)
      {
            Transaction tr = Transaction.begin(ObjectStore.READONLY);
            User user = (User) allUsers.get(userName);
            if (user == null) {
                  tr.abort(ObjectStore.RETAIN_HOLLOW);
                  throw new PersonalizationException ("Could not find user: " + userName );
            }
            if (user.getPIN() != PIN) {
                  tr.abort(ObjectStore.RETAIN_HOLLOW);
                  throw new PersonalizationException ("Invalid PIN for user: " + userName );
            }
            tr.commit(ObjectStore.RETAIN_READONLY);
            return user;
      }
If the userName and PIN passed to the validateUser() method denote a registered user, the validateUser() method returns a User object. Since User objects are persistent objects, if you want their contents to be accessible outside the transaction in which they were fetched from the database, you must specify the ObjectStore.RETAIN_READONLY or ObjectStore.RETAIN_UPDATE option when you commit the transaction.

If you use the default ObjectStore.RETAIN_STALE option, the receiver gets a stale User object. This causes ObjectStore to throw an exception when the application tries to access the User object. If you specify the ObjectStore.RETAIN_HOLLOW option, the validateUser() method returns a reference to a User object, but not the contents of the User object. That is, no name, email, or PIN information is available. You can use the returned reference in a subsequent transaction.

Deleting Objects

When you delete objects in ObjectStore, you must

Example of Deleting an Object

To remove users from the personalization database, for example, the application calls the unSubscribe() method.

      public static void unsubscribe(String name)
            throws PersonalizationException
      {
            Transaction tr = Transaction.begin(ObjectStore.UPDATE);
            User user = (User) allUsers.get(name);
            if (user == null) {
                  tr.abort(ObjectStore.RETAIN_HOLLOW);
                  throw new PersonalizationException ("Could not find user: " + name);
            }
            /* remove the user from the allUsers collection, and
             * remove all of the users interests from the allInterests collection */
            allUsers.remove(name);
            Iterator interests = user.getInterests().values().iterator();
            while (interests.hasNext())
                  allInterests.remove(interests.next());
            /* finally destroy the user and all its subobjects */
            ObjectStore.destroy(user);
            tr.commit(ObjectStore.RETAIN_HOLLOW);
      }
First, the Personalization application ensures that the user exists in the allUsers collection. If the user does not exist, ObjectStore throws an exception. Next, the application calls the remove() method to remove the user from the allUsers collection. This disconnects the user from the set of users, which means that this user is no longer reachable and can now be removed from the database. However, there are still interests associated with the user, so the application then removes all the user's interests from the allInterests collection. Finally, to remove the user from the database, the application calls destroy() on the User object.

Destroying an Object

The destroy() method is an operation defined on the ObjectStore class. The signature is

public static void destroy(Object object)
The object you specify must be persistent or the call has no effect.

By default, when you destroy an object, ObjectStore does not destroy objects that the destroyed object references. In the Personalization application, the User object references two strings, name and email, as well as a map of Interests. To destroy these objects along with the User object that references them, the application must define the IPersistent.preDestroyPersistent() hook method.

Destroying Objects Referenced by Destroyed Objects

When an application calls the ObjectStore.destroy() method, ObjectStore calls the preDestroyPersistent() method before actually destroying the specified object. A user-defined class should override this method to destroy any internal persistent data structures that it references. In the Personalization application, the preDestroyPersistent() method, as defined on the User class, looks like the following:

public void preDestroyPersistent() 
{
            if (!ObjectStore.isDestroyed(name))
                  ObjectStore.destroy(name);
            if (!ObjectStore.isDestroyed(email))
                  ObjectStore.destroy(email);
            /* destroy each of the interests */
            Iterator interestIter = interests.values().iterator();
            while (interestIter.hasNext()) {
                  Interest interest = (Interest) interestIter.next();
                  ObjectStore.destroy(interest);
            }
            /* destroy the interests list too */
            ObjectStore.destroy(interests);
      }
When you call the ObjectStore.destroy() method on an object, it removes the primitive objects that are referenced by this object, but it does not destroy fields in the object that are String, Long, or Double types. So for User objects, the PIN attribute, which is an int, is automatically deleted. But the application must explicitly destroy the name and email strings. In addition, destroying a Map does not touch any of the objects referenced by that map. Therefore, the application must iterate over the map of interests and destroy each of the Interest objects before destroying the map itself.

Destroying Strings

Before deleting the email and name strings, the application checks whether they have already been destroyed. If the user's name and email happened to have the same value, they could refer to the same String object in the database. This is because ObjectStore supports String pooling where only one copy of the String is stored in the database. Because Java Strings are immutable, this presents a problem when you explicitly delete a String object. See ObjectStore Java API User Guide, Destroying Strings.

About the Persistent Garbage Collector

ObjectStore provides a persistent garbage collector utility that automatically handles removing objects from the database. When using ObjectStore, you do not have to concern yourself with deleting objects. You are responsible for disconnecting your objects from relationships and associations, but ObjectStore can take care of removing all unreachable objects from the database.

The primary advantage of using the persistent garbage collector is that you do not have to write code to delete objects from the database. This allows you to avoid the problems associated with explicit deletion, such as dangling references and orphaned objects. See ObjectStore Java API User Guide, Performing Garbage Collection in a Database.

Using Collections

Sun's JDK 1.2 has been enhanced with support for collections. A collection (also known as a container) is a single object (such as Java's familiar Vector class) that represents a group of objects. The JDK 1.2 collections API is a unified framework for representing and manipulating collections, and allowing them to be manipulated independent of the details of their representation.

Collections, including Lists, Sets, Maps, and others help improve reuse and interoperability. They allow you to implement generic, reusable data objects that conform to the standard Collection interfaces. They also enable you to implement algorithms that operate on Collection objects independent of the details of their representation.

ObjectStore provides several collection implementations that mirror the Collection interfaces defined in the JDK 1.2 and that provide improved scalability. These include:

ObjectStore supports the hash table and vector representations, which are designed to provide good performance for persistent collections. OSTreeSet and OSTreeMap are based on a new B-tree implementation that is designed specifically for persistent collections with very large extents (hundreds of thousands of entries). Rather than causing your Java application to wait for all objects to be read into the collection from the database when the collection is first accessed, ObjectStore loads objects dynamically, several at a time, as your application navigates the elements in the collection.

The Personalization application uses a Map (OSHashMap) to keep track of all registered users. The user's name is the key for the map. In addition, the User class uses a Map to store the related interests that a particular user has.

Maps introduce a new requirement for classes of objects that will be stored as keys in persistent collections: these classes must provide a suitable hashCode() method. Objects that are stored as keys in Maps must provide hash codes that remain the same across transactions. The default Object.hashCode() method supplies an identity-based hash code. This identity hash code might depend on the virtual memory address or some internal implementation-level metadata associated with the object. Such a hash code is unsuitable for use in a persistent identity-based Map because it might be different each time an object is fetched from the database.

For your persistence-capable classes, you can override the hashCode() method and supply your own, or you can rely on the class file postprocessor to supply a hashCode() method suitable for storing instances in persistent hash tables. See ObjectStore Java API User Guide, Storing Objects as Keys in Persistent Hash Tables, for more information about supplying your own hashCode() methods.

The Personalization application uses a set (OSHashSet) to keep track of all interests that are defined. Since the Interest objects are stored in Maps that are part of the User objects, the application does not need the allInterests set to make Interest objects persistent. The allInterests set is useful since it allows you to efficiently perform queries, such as "find all users who have a particular interest."

For information on querying and indexing collections, see Chapter 5, Using ObjectStore to Query a Database.



[previous] [next]

Copyright © 1998 Object Design, Inc. All rights reserved.

Updated: 10/07/98 07:05:49