Other Classes That Participate in Object Graph Management

EOEditingContexts work in conjunction with instances of other classes to manage the object graph. Two other classes that play a significant role in object graph management are NSUndoManager and EOObserverCenter. NSUndoManager objects provide a general purpose undo stack. As a client of NSUndoManager, EOEditingContext registers undo events for all changes made the enterprise objects that it watches.

EOObserverCenter provides a notification mechanism for an observing object to find out when another object is about to change its state. "Observable" objects (typically all enterprise objects) are responsible for invoking their willChange method prior to altering their state (in a "set" method, for instance). Objects (such as instances of EOEditingContext) can add themselves as observers to the objects they care about in the EOObserverCenter. They then receive a notification (as an objectWillChange message) whenever an observed object invokes willChange.

The objectWillChange method is defined in the EOObserving interface. EOEditingContext implements the EOObserving interface.


Programmatically Creating an EOEditingContext

Typically, an EOEditingContext is created automatically for the application as a by product of some other operation. For example, the following operations result in the creation of network of objects that include an EOEditingContext:

Under certain circumstances, however, it may be needed to create an EOEditingContext programmatically for example, if you are writing an application that doesn't include a graphical interface. To create an EOEditingContext, the following are to be done:

  EOEditingContext editingContext = new EOEditingContext();

This creates an editing context that's connected to the default EOObjectStoreCoordinator. You can change this default setting by initializing an EOEditingContext with a particular parent EOObjectStore. This is useful if you want the EOEditingContext to use a different EOObjectStoreCoordinator than the default, or if the EOEditingContext is nested. For example, the following code excerpt initializes childEditingContext with a parent object store parentEditingContext:

  EOEditingContext parentEditingContext;     // Assume this exists.
  EOEditingContext childEditingContext = new EOEditingContext
  (parentEditingContext);
You are responsible for ensuring thread safe handling of any EOEditingContexts you create programmatically by using the lock and unlock methods.

Accessing An Editing Context's Adaptor Level Objects

You can use an EOEditingContext with any EOObjectStore. However, in a typical configuration, you uses an EOEditingContext with the objects in the access layer. To access an EOEditingContext's adaptor level objects, you get the editing context's EOObjectStoreCoordinator from the editing context, an EODatabaseContext (EOAccess) from the object store coordinator, and the adaptor level objects from there. The following code demonstrates the process.

  EOEditingContext editingContext;  // Assume this exists.
  String entityName;                // Assume this exists.
  EOFetchSpecification fspec;
  EOObjectStoreCoordinator rootStore;
  com.webobjects.eoaccess.EODatabaseContext dbContext;
  com.webobjects.eoaccess.EOAdaptor adaptor;
  com.webobjects.eoaccess.EOAdaptorContext adContext;
  fspec = new EOFetchSpecification(entityName, null, null);
  rootStore = (EOObjectStoreCoordinator)editingContext.rootObjectStore();
  dbContext = (EODatabaseContext)rootStore.objectStoreForFetchSpecification(fspec);
  adaptor = dbContext.database().adaptor();
  adContext = dbContext.adaptorContext();

This example first creates a fetch specification, providing just entity name as an argument. Of course, you can use a fetch specification that has non null values for all of its arguments, but only the entity name is used by the EOObjectStore objectStoreForFetchSpecification method. Next, the example gets the editing context's EOObjectStoreCoordinator using the EOEditingContext method rootObjectStore. rootObjectStore returns an EOObjectStore and not an EOObjectStoreCoordinator, because it's possible to substitute a custom object store in place of an object store coordinator. Similarly, the EOObjectStoreCoordinator method objectStoreForFetchSpecification returns an EOCooperatingObjectStore instead of an access layer EODatabaseContext because it's possible to substitute a custom cooperating object store in place of a database context. If the code performs any such substitutions, you should alter the above code example to match the custom object store's API.

An EOEditingContext's EOObjectStoreCoordinator can have more than one set of database and adaptor level objects. Consequently, to get a database context from the object store coordinator, you have to provide information that the coordinator can use to choose the correct database context. The code example above provides an EOFetchSpecification using the method objectStoreForFetchSpecification, but you could specify different criteria by using one of the following EOObjectStoreCoordinator methods instead:


Method Description
cooperatingObjectStores Returns an array of the EOObjectStoreCoordinator's cooperating object stores.
objectStoreForGlobalID Returns the cooperating object store for the enterprise object identified by the provided EOGlobalID.
objectStoreForObject Returns the cooperating object store for the provided enterprise object.

After you have the EODatabaseContext, you can get the corresponding EOAdaptor and EOAdaptorContext as shown above. (EODatabaseContext, EOAdaptor, and EOAdaptorContext are all defined in EOAccess.)


Using EOEditingContexts in Different Configurations

The fundamental relationship an EOEditingContext has is to its parent EOObjectStore, which creates the object graph the EOEditingContext monitors. EOObjectStore is an abstract class that defines a source and sink of objects for an EOEditingContext. The EOObjectStore is responsible for constructing and registering objects, servicing object faults, and committing changes made in an EOEditingContext.

You can augment the basic configuration of an EOEditingContext and its parent EOObjectStore in several different ways. For example, multiple EOEditingContexts can share the same EOObjectStore, one EOEditingContext can act as an EOObjectStore for another, and so on. The most commonly used scenarios are described in the following sections.


Peer EOEditingContexts

One or more "peer" EOEditingContexts can share a single EOObjectStore. Each EOEditingContext has its own object graph so, for example, a given Employee row in a database can have separate object instances in each EOEditingContext. Changes to an object in one EOEditingContext don't affect the corresponding object in another EOEditingContext until all changes are successfully committed to the shared object store. At that time the objects in all EOEditingContexts are synchronized with the committed changes. This arrangement is useful when an application allows the user to edit multiple independent "documents."

Nested EOEditingContexts

EOEditingContext is a subclass of EOObjectStore, which gives its instances the ability to act as EOObjectStores for other EOEditingContexts. In other words, EOEditingContexts can be nested, thereby allowing a user to make edits to an object graph in one EOEditingContext and then discard or commit those changes to another object graph (which, in turn, may commit them to an external store). This is useful in a "drill down" style of user interface where changes in a nested dialog can be okayed (committed) or canceled (rolled back) to the previous panel.

Nested EOEditingContexts

When an object is fetched into a nested EOEditingContext, it incorporates any uncommitted changes that were made to it in its parent EOEditingContext. For example, suppose that in one panel you have a list of employees that allows you to edit salaries, and that the panel includes a button to display a nested panel where you can edit detail information. If you edit the salary in the parent panel, the modified salary is seen in the nested panel, not the old (committed) salary from the database. Thus, conceptually, nested EOEditingContexts fetch through their parents.

EOEditingContext overrides several of EOObjectStore's methods:

These methods are generally used when an EOEditingContext acts as an EOObjectStore for another EOEditingContext.


Getting Data from Multiple Sources

An EOEditingContext's object graph can contain objects from more than one external store. In this scenario, the object store is an EOObjectStoreCoordinator, which provides the abstraction of a single object store by redirecting operations to-one or more EOCooperatingObjectStores.

In writing an application, it's likely that you will use combinations of the different scenarios described in the above sections.


Fetching Objects

The most common way to explicitly fetch objects from an external store in an Enterprise Objects Framework application is to use EOEditingContext's objectsWithFetchSpecification method. This method takes a fetch specification and returns an array of objects. A fetch specification includes the name of the entity for which you want to fetch objects, the qualifier (query) you want to use in the fetch, and the sort order in which the objects need to be returned (if any).


Managing Changes in the Application

EOEditingContext provides several methods for managing the changes made to objects in the application. You can use these methods to get information about objects that have changed, to selectively undo and redo changes, and to discard all changes made to objects before these changes are committed to the database. These methods are described in the following sections.


Getting Information About Changed Objects

An EOEditingContext maintains information about three different kinds of changes to objects in its object graph: insertions, deletions, and updates. After these changes have been made and before they're committed to the database, you can find out which objects have changes in each of these categories by using the insertedObjects, deletedObjects, and updatedObjects methods. Each method returns an array containing the objects that have been inserted, deleted, and updated, respectively. The hasChanges method returns true or false to indicate whether any of the objects in the object graph have been inserted, deleted, or updated.


Undo and Redo

EOEditingContext includes the undo, redo, and revert methods for managing changes to objects in the object graph. undo asks the EOEditingContext's NSUndoManager to reverse the latest changes to objects in the object graph. redo asks the NSUndoManager to reverse the latest undo operation. revert clears the undo stack, discards all insertions and deletions, and restores updated objects to their last committed (saved) values.

EOEditingContext's undo support is arbitrarily deep; you can undo an object repeatedly until you restore it to the state it was in when it was first created or fetched into its editing context. Even after saving, you can undo a change. To support this feature, the NSUndoManager can keep a lot of data in memory.

For example, whenever an object is removed from a relationship, the corresponding editing context creates a snapshot of the modified, source object. The snapshot, which has a reference to the removed object, is referenced by the editing context and by the undo manager. The editing context releases the reference to the snapshot when the change is saved, but the undo manager doesn't. It continues holding the snapshot, so it can undo the deletion if requested.

If the typical usage patterns for the application generate a lot of change processing, you might want to limit the undo feature to keep its memory usage in check. For example, you could clear an undo manager whenever its editing context saves. To do so, simply send the undo manager a removeAllActions message (or a removeAllActionsWithTarget message with the editing context as the argument). If the application doesn't need undo at all, you can avoid any undo overhead by setting the editing context's undo manager to null with setUndoManager.


Saving Changes

The saveChanges method commits changes made to objects in the object graph to an external store. When you save changes, EOEditingContext's lists of inserted, updated, and deleted objects are flushed.

Upon a successful save operation, the EOEditingContext's parent EOObjectStore broadcasts an ObjectsChangedInStoreNotification. Peers of the saved EOEditingContext receive this notification and respond by synchronizing their objects with the committed versions.


Methods for Managing the Object Graph

EOEditingContext provides methods for managing the enterprise objects in the context's object graph. This section describes these methods, as well as other techniques you can use to manage the object graph.

At different points in the application, you might want to do the following:

These scenarios are discussed in the following sections.


Discarding Changes to Enterprise Objects

EOEditingContext provides different techniques for discarding changes to enterprise objects. These techniques are as follows:

A different approach is to use the invalidate... methods, described in "Discarding the View of Objects Cached in Memory".


Refreshing Objects

One characteristic of an object graph is that it represents an internally consistent view of the application's data. By default, when you refetch data, Enterprise Objects Framework maintains the integrity of the object graph by not overwriting the object values with database values that have been changed by someone else. But what if you want the application to see those changes? You can accomplish this by using the EOFetchSpecification method setRefreshesRefetchedObjects. Invoking setRefreshesRefetchedObjects with the argument true causes existing objects to be overwritten with fetched values that have been changed. Alternatively, you can use the EODatabaseContext (EOAccess) delegate method databaseContextShouldUpdateCurrentSnapshot.

Normally, when you set an EOFetchSpecification to refresh using setRefreshesRefetchedObjects, it only refreshes the objects you are fetching. For example, if you refetch employees, you don't also refetch the employees' departments. However, if you also set the fetch specification to prefetch relationships, the refetch is propagated for all of the fetched objects' relationships that are specified with setPrefetchingRelationshipKeyPaths.

Refreshing refetched objects only affects the objects specified. If you want to refetch the entire object graph, you can use the EOEditingContext invalidate... methods, described below.


Discarding the View of Objects Cached in Memory

As described in the section, you can use undo or revert to selectively discard the changes made to enterprise objects. Using these methods preserves the original cache of values fetched from the database. But what if you want to flush the in memory object view all together in the most likely scenario, to see changes someone else has made to the database? You can invalidate the enterprise objects using the invalidateAllObjects method or the invalidateObjectsWithGlobalIDs method. (You can also use the method refetch, which simply invokes invalidateAllObjects). Unlike fetching with the EOFetchSpecification method setRefreshesRefetchedObjects set to true (described above), the invalidate... methods result in the refetch of the entire object graph.

The effect of the invalidateAllObjects method depends on how you use it. For example, if you send invalidateAllObjects to an EOEditingContext, it sends invalidateObjectsWithGlobalIDs to its parent object store with all the globalIDs for the objects registered in it. If the EOEditingContext is nested, its parent object store is another EOEditingContext; otherwise its parent object store is typically an EOObjectStoreCoordinator. Regardless, the message is propagated down the object store hierarchy. Once it reaches the EOObjectStoreCoordinator, it's propagated to the EODatabaseContext(s). The EODatabaseContext discards the row snapshots for these globalIDs and sends an ObjectsChangedInStoreNotification, thereby refaulting all the enterprise objects in the object graph. The next time you access one of these objects, it's refetched from the database.

Sending invalidateAllObjects to an EOEditingContext affects not only that context's objects, but objects with the same globalIDs in other EOEditingContexts. For example, suppose editingContext1 has objectA and objectB, and editingContext2 has objectA, objectB, and objectC. When you send invalidateAllObjects to editingContext1, objectA and objectB are refaulted in both editingContext1 and editingContext2. However, objectC in editingContext2 is left intact since editingContext1 doesn't have an objectC.

If you send invalidateAllObjects directly to the EOObjectStoreCoordinator, it sends invalidateAllObjects to all of its EODatabaseContexts, which then discard all of the snapshots in the application and refault every single enterprise object in all of the EOEditingContexts.

The invalidate... methods are the only way to get rid of a database lock without saving changes.

Frequent use of invalidateAllObjects or invalidateObjectsWithGlobalIDs can have significant performance impact. If you no longer need the EOs in the editing context, it may be faster to simply do:

  ec.dispose();
ec = new EOEditingContext();
// fetch or fault new objects into ec

Working with Objects Across Multiple EOEditingContexts

Any time the application is using more than one EOEditingContext as described in the section "Using EOEditingContexts in Different Configurations", it's likely that one editing context will need to access objects in another.

On the face of it, it may seem like the most reasonable solution would be for the first editing context to just get the desired object in the second editing context and modify the object directly. But this would violate the cardinal rule of keeping each editing context's object graph internally consistent. Instead of modifying the second editing context's object, the first editing context needs to get its own copy of the object. It can then modify its copy without affecting the original. When it saves changes, they're propagated to the original object, down the object store hierarchy. The method that you use to give one editing context its own copy of an object that's in another editing context is faultForGlobalID.

For example, suppose you have a nested editing context configuration in which a user interface displays a list of objects this maps to the parent editing context. From the list, the user can select an object to inspect and modify in a "detail view" this maps to the child editing context. To give the child its own copy of the object to modify in the detail view, you would do something like the following:

  EOEditingContext childEC, parentEC; // Assume these exist.
  Object origObject;                // Assume this exists.
  Object newObject;
  newObject = childEC.faultForGlobalID
  (parentEC.globalIDForObject(origObject), childEC);
  

where origObject is the object the user selected for inspection from the list.

The child can make changes to newObject without affecting origObject in the parent. Then when the child saves changes, origObject is updated accordingly.


Updates from the Parent EOObjectStore

When changes are successfully saved in an EOObjectStore, it broadcasts an ObjectsChangedInStoreNotification. An EOEditingContext receiving this notification synchronizes its objects with the committed values by refaulting objects needing updates so the new values are retrieved from the EOObjectStore the next time they are needed. However, locally uncommitted changes to objects in the EOEditingContext are by default reapplied to the objects, in effect preserving the uncommitted changes in the object graph. After the update, the uncommitted changes remain uncommitted, but the committed snapshots have been updated to reflect the values in the EOObjectStore.

You can control this process by implementing two delegate methods. Before any updates have occurred, the delegate method editingContextShouldMergeChangesForObject is invoked for each of the objects that has both uncommitted changes and an update in the EOObjectStore. If the delegate returns true, the uncommitted changes are merged with the update (the default behavior). If it returns false, then the object is invalidated (and refaulted) without preserving any uncommitted changes. As a side effect, the delegate might cache information about the object (globalID, snapshot, etc.) so that a specialized merging behavior could be implemented. At this point, the delegate should not make changes to the object because it is about to be invalidated. However, the delegate method editingContextDidMergeChanges is invoked after all of the updates for the ObjectsChangedInStoreNotification have been completed, including the merging of all uncommitted changes. By default, it does nothing, but this delegate method might perform the customized merging behavior based on whatever information was cached in editingContextShouldMergeChangesForObject for each of the objects that needed an update.


General Guidelines for Managing the Object Graph

When you fetch objects into the application, you create a graph of objects instantiated from database data. From that point on, the focus should be on working with the object graph not on interacting with the database. This distinction is an important key to working with Enterprise Objects Framework.


You do not have to worry about the database...

One of the primary benefits of Enterprise Objects Framework is that it insulates you from having to worry about database details. Once you have defined the mapping between the database and the enterprise objects in a model file, you do not need to think about issues such as foreign key propagation, how object deletions are handled, how operations in the object graph are reflected in the database tables, and so on.

This can be illustrated by considering the common scenario in which one object has a relationship to another. For example, suppose an Employee has a relationship to a Department. In the object graph, this relationship is simply expressed as an Employee object having an instance variable for its Department object. The Department object might in turn have an instance variable that's an array of Employee objects. When you manipulates relationships in the object graph (for example, by moving an Employee to a different Department), Enterprise Objects Framework changes the appropriate relationship references. For example, moving an Employee to a different Department changes the Employee's department instance variable and adds the Employee to the new Department's employee array. When you save the changes to the database, Enterprise Objects Framework knows how to translate these object graph manipulations into database operations.


...but you have to worry about the object graph

As described above, you generally doesn't need to concern with how changes to the object graph are saved to the database. However, you do not need to concern yourself with managing the object graph itself. Since the object graph is intended to represent an internally consistent view of the application's data, one of the primary considerations should be maintaining its consistency. For example, suppose you have a relationship from Employee to Project, and from Employee to Manager. When you create a new Employee object, you must make sure that it has relationships to the appropriate Projects and to a Manager.

Just as you need to maintain the internal consistency of an EOEditingContext's object graph, you should never directly modify the objects in one EOEditingContext from another EOEditingContext. If you do so, you risks creating major synchronization problems in the application. If you need to access the objects in one EOEditingContext from another,the method faultForGlobalID can be used, as described in "Working with Objects Across Multiple EOEditingContexts" . This gives the receiving EOEditingContext its own copy of the object, which it can modify without affecting the original. Then when it saves its changes, the original is updated accordingly.

One of the implications of needing to maintain the consistency of the object graph is that you should never copy an enterprise object (though you can snapshot its properties), since this would be in conflict with uniquing. Uniquing dictates that an EOEditingContext can have one and only one copy of a particular object. Similarly, enterprise objects shouldn't override the equals method. Enterprise Objects Framework relies on this method checking implementation which checks instance equality rather than value equality.


Using EOEditingContext to Archive Objects with WebObjects Framework

As of WebObjects 5, the WebObjects Frameworks use Java Serialization to archive objects in a persistent platform independent manner. For general information about Serialization see the java.io package and Object Serialization

Many of the classes provided by the Foundation and EOControl frameworks implement the java.io.Serializable interface. You can archive and unarchive these classes with java.io.ObjectInputStream and ObjectOutputStream objects like any other Serializable objects.

JavaClient and its EODistribution framework as well as some legacy applications still use the NSCoding style archiving. This is essentially deprecated since WebObjects 5. New applications should all use Java Serialization whenever possible.

Serializing EOEnterpriseObjects

Both EOEnterpriseObject and EOEditingContext implement Serializable. Archiving an EOEditingContext saves the context's current state, any pending updates to its enterprise objects, and finally serializes all the enterprise objects managed by that EOEditingContext. An EOEditingContext stores only as much information about its enterprise objects as necessary to reconstitute the object graph later. For example, unmodified objects are stored as simple references (by EOGlobalID) that will allow the context to recreate the object from the database. Thus, the application can store state very efficiently by letting an EOEditingContext archive the enterprise objects. Essentially enterprise objects are only archived and unarchived as faults whenever possible. Updated objects have the delta from their last committed state archived, and inserted objects are archived in their entirety.

EOEditingContext includes several additional methods that affect the archiving and unarchiving of objects: setUsesContextRelativeEncoding and usesContextRelativeEncoding. This is true by default. If usesContextRelativeEncoding returns false, serializing an enterprise object will archive all the properties and all of the state for that object, not just a EOGlobalID, even if it is unmodified. This creates a great deal more data, but can be useful for creating a database independent archive. Such an archive can be passed to others without access to your database, or used to transition between databases.

It also has the methods setSubstitutionEditingContext and substitutionEditingContext. These control which EOEditingContext deserialized enterprise objects are placed into. Unless you have set an explicit substitution context, the EOEnterpriseObjects will be deserialized into the EOEditingContext in the serialized stream (i.e. a transitory EOEditingContext for which you do not have a reference). It can be useful within a WOSession to do:

setSubstitutionEditingContext(defaultEditingContext());
Object results = myObjectInputStream.readObject();
setSubstitutionEditingContext(null);
This places all the unarchived enterprise objects in the context associated with your current session.

Both the substitution editing context and the context relative encoding flag are global settings. It is important to ensure multiple threads do not try to change either setting while a separate thread is archiving or unarchiving EOEnterpriseObjects or EOEditingContexts. If you intend to have several threads potentially Serializing enterprise objects simultaneously, you should create a separate lock object, such as an NSLock, to manage potential contention.