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:
Creating Database Entry Points
Accessing Objects in the Database
Getting Ready to Store Objects
Before you can create and manipulate persistent objects with ObjectStore, you must perform the following operations:
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.
/** * Close the database and terminate the session. */ public static void shutdown() { db.close(); session.terminate(); }
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.
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. 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. 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().
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.
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.
allUsers = (Map) db.getRoot("allUsers");
/* 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.
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.
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.
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.
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.
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.
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.
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."
Updated: 10/07/98 07:05:49