Update Strategies

The applications you build with Enterprise Objects will likely be used simultaneously by many different users. These users usually can access the same set of data and probably have the rights to update all or part of that data set. But what happens when multiple users try to update the same data simultaneously? How do you prevent users from overwriting other user’s work? This realm of database application development is referred to as update conflicts.

This chapter discusses the update strategies available in Enterprise Objects. It is divided into the following sections:

Choosing a Strategy

An update strategy determines how update conflicts should be handled. The most common update strategy in database application development is locking. A locking strategy represents a preventative approach to managing update conflicts. There are a few different locking strategies, the most common of which is pessimistic locking. Using the pessimistic locking approach, a row of data in the database is locked when it is fetched to prevent other users from accessing that row of data.

It is generally a poor approach in three-tier database applications. Of the many reasons that make it a poor approach, perhaps the most important is that of all the data a given user fetches, they are likely to edit only a small amount of that data. When you implement a pessimistic locking strategy, however, you prevent other users from even viewing the data that one user has accessed. So the pessimistic approach severely impacts the usability of your application and compromises the value of the application’s data (since users aren’t guaranteed that they can see all the data).

Although a pessimistic locking strategy largely guarantees that update conflicts won’t occur, pessimistic locking has these undesirable effects:

Consistent with Enterprise Object’s abstract character, it provides a locking strategy that works at a higher level than the database. This approach is called optimistic locking. With optimistic locking, database rows are never actually locked. This strategy doesn’t detect update conflicts until an application attempts to save changes to the database. Optimistic locking provides these advantages:

The recommended update strategy in an Enterprise Objects application is optimistic locking. In addition to pessimistic locking, Enterprise Objects supports other locking strategies but they are not generally appropriate for three-tier application environments.

Inside Optimistic Locking

By default, Enterprise Objects uses optimistic locking to manage update conflicts. The idea behind optimistic locking is rather simple: When a user attempts to save changes made to an enterprise object, the framework compares the data in that object’s snapshots with the current data in the database. If the comparison yields no differences, the save is allowed to execute.

In more detail, when data is fetched from a data source, Enterprise Objects records a snapshot for each row of data that is fetched. It stores these snapshots in an EODatabaseContext object. From these snapshots, enterprise object instances are created. When users make changes to the data in enterprise object instances and attempt to commit those changes back to the data source, the framework finds the object’s corresponding row-level snapshots and identifies the locking attributes in those snapshots. In the UPDATE statement, it uses these values in a WHERE clause to make sure the row that is being updated hasn’t changed in the database since it was last fetched.

If the update operation returns zero rows, it means that the values in the columns referenced in the update’s WHERE clause changed, which means that the data in the database that corresponds to the enterprise object instance that is trying to save has been changed by another user or process, and an optimistic locking exception is thrown.

Consider this example. Using the entity described in Reference Entity, suppose a Listing enterprise object is fetched and has the following data values:

Then suppose that a user changes the value of the bathrooms property to 2.5. When the user attempts to save that change, the following SQL is generated: UPDATE LISTING SET BATHROOMS = 2.5 WHERE (BATHROOMS = 2 AND BEDROOMS = 4).

The columns specified in the WHERE clause include only the attributes that are marked for locking in the application’s EOModel. If this update returns zero rows, it means that the condition of the WHERE clause isn’t satisfied, which means that the enterprise object’s data in the database changed from the time it was last fetched—an optimistic locking failure.

When a user makes changes to data and attempts to save those changes, they must be reasonably guaranteed that the data they edited represents the freshest state of the data in the database. Simply committing a user’s changes back to the data source without determining if the data in the data source has changed compromises data consistency and integrity.

Multiple Coordinators and Optimistic Locking

In some ways, when you provide each session with its own access layer stack, you complicate the optimistic locking equation. When multiple users share the same row-level snapshots, in contrast, you minimize the opportunities for multiple users to cause an optimistic locking conflict.

Consider the case when user A and user B share row-level snapshots and both users open a record that contains the same row of data. When user A make changes to that row and commits those changes back to the data source, the snapshot of that row reflects user A’s changes. When user B requests the same record after user A changes it, user B is guaranteed to see the data with user A’s changes. In this scenario, both users share the same set of cached data.

Then consider the case when user A and user B don’t share row-level snapshots. When user A makes changes to a row and commits those changes back to the data source, user B doesn’t see those changes unless they explicitly request fresh data from the database. When user B changes the same record and attempts to commit those changes, an optimistic locking exception is thrown since user B was editing stale data. (The exception usually occurs in the method updateValuesInRowDescribedByQualifier in a EOAdaptorChannel subclass).

If you want to provide each session with an independent access layer stack, there are a number of workarounds to deal with the optimistic locking issues that result from that configuration. As discussed in Ensuring Fresh Data, you can explicitly set the fetch timestamp on an editing context to encourage refetching from the data source rather than from the access layer’s row-level snapshots. While this causes an inordinate amount of fetching, depending on the average size of an application’s data sets, it may be a viable option.

Another workaround is to perform raw row operations—operations that don’t automatically result in the creation of enterprise object instances from fetched data. The results of raw row operations—such as raw row fetching, raw SQL operations, or fetch specifications that fetch raw rows—are not cached so you don’t need to worry about optimistic locking. The optimistic locking mechanism is effectively bypassed for raw row operations. This has other significant consequences as discussed in Raw Row Fetching.

Using Optimistic Locking

Although optimistic locking is enabled by default in an Enterprise Objects application, you still need to make decisions that affect how it works. At minimum, you need to select which attributes in your application’s entities participate in optimistic locking. You identify an attribute as a participant in optimistic locking by selecting its locking characteristic in EOModeler. Attributes that are selected for locking appear with a lock icon in their row.

By default, all attributes you add to entities other than primary keys are selected for optimistic locking. However, selecting all types of attributes for optimistic locking is not optimum and can result in serious performance implications. Consider these guidelines when choosing which attributes to select for locking:

Prevention

Before determining how to instrument your application to deal with update conflicts, you should understand the mechanisms Enterprise Objects uses to prevent update conflicts, and especially how you can utilize these mechanisms in your applications.

Within Enterprise Objects, there are a number of contexts in which the preventative mechanisms for avoiding update conflicts work. These contexts include application instances, individual sessions, and an application’s data sources.

Within a given application instance, an Enterprise Objects application that uses a single access layer stack (as described in Core Framework Stack provides a single database context (per data source) for all the application’s sessions. When sessions share a database context, they share row-level snapshots that participate in the update strategy called optimistic locking. By sharing row-level snapshots, multiple sessions are less likely to encounter optimistic locking exceptions caused by other sessions since the shared snapshots contribute to fresher data in each session than if the snapshots aren’t shared.

Within a given session, Enterprise Objects provides a change notification infrastructure that updates in-memory enterprise object instances when the data in other enterprise object instances changes. This helps to ensure that within a given session, a user sees the freshest data throughout, especially when they’ve changed data. This helps minimize the possibility of a given session from triggering an optimistic locking failure based on data that is edited within that session. For example, if a session edits a Listing enterprise object early in the session and then later edits it again, notifications ensure that the second time the Listing object is edited, it reflects the changes made in the first edit. Otherwise, sessions would likely overwrite their own data, causing optimistic locking failures.

Finally, within a database that an Enterprise Objects application uses, optimistic locking ensures that one user’s changes aren’t overwritten by another user’s changes. How this works is discussed in Inside Optimistic Locking.

Mechanisms within each of these three realms contribute to the prevention of update conflicts. In most applications, you’ll need to take some control over the mechanisms in each realm. Perhaps the most common type of intervention is adjusting an editing context’s fetch timestamp to encourage more fetching from the database, which ensures that an application’s user interface reflects fresh data. It is discussed in Ensuring Fresh Data.

Recovery

Instrumenting your applications to prevent update conflicts may not be enough to deal with the problem. It’s also prudent to instrument your applications to recover gracefully when an update conflict occurs. This section discusses what you need to think about to instrument recovery and provides code samples that can help you implement a recovery strategy.

When an optimistic locking conflict is detected, an EOGeneralAdaptorException is thrown. You can do a number of things to deal with this exception. By default, Enterprise Objects doesn’t do anything when the exception is thrown. A common design pattern is to wrap an invocation of EOEditingContext.saveChanges() in a try-catch block. In the catch block, you can choose to do a number of things.

You can choose to do nothing, which simply hides the exception from the user. You can choose to identify the affected enterprise objects, refault them, tell the user to make their changes and try saving again, as discussed in Recovering and Refaulting. You can choose to identify the affected enterprise objects, identify the changes that failed to save, and choose to save those changes again, disregarding the data in the database, as discussed in Recovering and Last Write Wins.

Recovering and Refaulting

The following example catches an optimistic locking failure, identifies the enterprise objects involved in the failure, and refaults those objects.

To catch an optimistic locking failure, you typically add a try-catch block around an invocation of EOEditingContext.saveChanges(), as shown in Listing 9-1.

Listing 8-1  Adding a try-catch block around saveChanges

public void save() {
        EOEditingContext editingContext = session().defaultEditingContext();
        try {
            editingContext.saveChanges();
            //Thrown for each eo that fails to save.
        } catch (EOGeneralAdaptorException saveException) { // 1
            //Determine if the exception is an optimistic locking exception.
            if (isOptimisticLockingFailure(saveException)) {// 2
                //Deal with the optimistic locking exception.
                handleOptimisticLockingFailure(saveException);// 3
            } else {
                //Don't know what went wrong so revert editing context to a stable state.
                editingContext.revert();// 4
            }
        }
 
    }

Code line 1 catches the exception that is thrown when a save fails, which is usually an EOGeneralAdaptorException. The method invoked in code line 2 determines if the exception is an optimistic locking failure. The method invoked in code line 3 deals with the failure. If another kind of exception is thrown during the save, code line 4 simply invokes revert on the editing context, which returns the editing context to a stable state.

The code sample in Listing 9-2 determines if the exception thrown is an optimistic locking failure. It is invoked from code line 2 in Listing 9-1.

Listing 8-2  Determining if the exception is an optimistic locking failure

//Determine if the exception thrown during a save is an optimistic locking exception.
    public boolean isOptimisticLockingFailure(EOGeneralAdaptorException                                                            exceptionWhileSaving) {
        //Get the info dictionary that is created when the exception is thrown.
        NSDictionary exceptionInfo = exceptionWhileSaving.userInfo();// 1
        //Determine the type of the failure.
        Object failureType = (exceptionInfo != null) ?              exceptionInfo.objectForKey(EOAdaptorChannel.AdaptorFailureKey) : null;// 2
        //Return depending on the type of failure.
        if ((failureType != null) &&              (failureType.equals(EOAdaptorChannel.AdaptorOptimisticLockingFailure))) {// 3
            return true;
        } else {
            return false;
        }
    }

Throughout Enterprise Objects, many of the possible exceptions that are thrown include an info dictionary that provides details about the causes of an exception. Code line 1 simply retrieves the info dictionary from the exception thrown during the optimistic locking failure. Code line 2 uses that dictionary to determine the type of failure. Code line 3 returns true if the failure type is an optimistic locking failure and false otherwise.

After the method in Listing 9-1 determines if the exception resulted from an optimistic locking failure, the code sample in Listing 9-3 manages the failure. The method in Listing 9-3, handleOptimisticLockingFailureByRefaulting, is invoked in code line 3 of Listing 9-1.

Listing 8-3  Managing an optimistic locking failure by refaulting

//Deal with an optimistic locking failure.
    public void handleOptimisticLockingFailureByRefaulting(EOGeneralAdaptorException         lockingException) {
        //Get the info dictionary that is created when the exception is thrown.
        NSDictionary info = lockingException.userInfo();// 1
        //Determine the adaptor operation that triggered the optimistic locking failure.
        EOAdaptorOperation adaptorOperation =     (EOAdaptorOperation)info.objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey);// 2
        int operationType = adaptorOperation.adaptorOperator();// 3
        //Determine the database operation that triggered the failure.
        EODatabaseOperation dbOperation = (EODatabaseOperation)info.objectForKey(EODatabaseContext.FailedDatabaseOperationKey);// 4
        //Retrieve the enterprise object that triggered the failure.
        EOEnterpriseObject failedEO = (EOEnterpriseObject)dbOperation.object();// 5
        //Retrieve the dictionary of values involved in the failure.
        //Take action based on the type of adaptor operation that triggered the optimistic          locking failure.
        if (operationType == EODatabaseOperation.AdaptorUpdateOperator) {// 6
            //Recover by refaulting the enterprise object involved in the failure.
            //This refreshes the eo's data and allows the user to enter changes again               and resave.
            session().defaultEditingContext().refaultObject(failedEO);// 7
            }
        } else { //The optimistic locking failure was caused by another type of adaptor             operation, not an update.
            throw new NSForwardException(lockingException, "Unknown adaptorOperator " +               operationType + " in optimistic locking exception.");
        }
        session().defaultEditingContext().saveChanges();
    }

Code line 1 retrieves the info dictionary that contains detailed information about the optimistic locking failure. Code line 2 determines the adaptor operation that triggered the failure.

Code line 3 determines the type of the adaptor operation that triggered the failure. There are a number of adaptor operations that include AdaptorUpdateOperator and AdaptorDeleteOperator. See the API reference for the class com.webobjects.eoaccess.EOAdaptorOperation for a list of all the operations. The adaptor operation type is used in code line 6.

Code line 4 retrieves the database operation in which the optimistic locking failure originated. From the database operation, code line 5 retrieves the enterprise object that was involved in the locking failure. An optimistic locking exception is thrown when an individual enterprise object instance fails to save. When even a single enterprise object fails to save, the entire saveChanges operation fails.

From the information retrieved by the code in code line 3, code line 6 takes action based on the type of adaptor operation. You need to determine which adaptor operation triggered the optimistic locking failure to know how to manage the failure. You’d manage a failure resulting from an AdaptorDeleteOperator differently than you’d manage a failure resulting from an AdaptorUpdateOperator.

After determining that the exception thrown during saveChanges resulted from an optimistic locking failure, code line 7 attempts to refault the enterprise object that was involved in the failure. Refaulting clears in-memory changes in an enterprise object and populates its data with values from the database. Users would then need to reenter their changes and attempt to save again, so you should display a message in the user interface with those instructions.

Recovering and Last Write Wins

The following example catches an optimistic locking failure, identifies the enterprise objects involved in the failure, identifies the values of the enterprise objects involved in the failure, and attempts to commit those values to the database.

Listing 8-4  Managing an optimistic locking failure by last write wins

//Deal with an optimistic locking failure.
    public void handleOptimisticLockingFailureByLastWriteWins(EOGeneralAdaptorException lockingException) {
        //Get the info dictionary that is created when the exception is thrown.
        NSDictionary info = lockingException.userInfo();
        //Determine the adaptor operation that triggered the optimistic locking failure.
        EOAdaptorOperation adaptorOperation = (EOAdaptorOperation)info.objectForKey(EOAdaptorChannel.FailedAdaptorOperationKey);
        int operationType = adaptorOperation.adaptorOperator();
        //Determine the database operation that triggered the failure.
        EODatabaseOperation dbOperation = (EODatabaseOperation)info.objectForKey(EODatabaseContext.FailedDatabaseOperationKey);
        //Retrieve the enterprise object that triggered the failure.
        EOEnterpriseObject failedEO = (EOEnterpriseObject)dbOperation.object();
        //Retrieve the dictionary of values involved in the failure.
        NSDictionary valuesInFailedSave = adaptorOperation.changedValues();// 1
        NSLog.out.appendln("valuesInFailedSave: " + valuesInFailedSave);
 
        //Take action based on the type of adaptor operation that triggered the optimistic locking failure.
        if (operationType == EODatabaseOperation.AdaptorUpdateOperator) {
            //Recover by essentially ignoring the optimistic locking failure and committing the
            //changes that originally failed. This is a last write wins policy.
            //Overwrite any changes in the database with the eo's values.
            failedEO.reapplyChangesFromDictionary(valuesInFailedSave); // 2
        } else { //The optimistic locking failure was causes by another type of adaptor operation, not an update.
            throw new NSForwardException(lockingException, "Unknown adaptorOperator " + operationType + " in optimistic locking exception.");
        }
        session().defaultEditingContext().saveChanges();
    }

Listing 9-4 differs from Listing 9-3 only in code line 1 and code line 2. Code line 1 of Listing 9-4 retrieves the values of the enterprise object that were involved in the optimistic locking failure. For example, if the enterprise object represents the Listing entity in the Real Estate model and the bedrooms attribute of that enterprise object had changes, the dictionary of changes retrieved in code line 1 contains a key called bedrooms and its changed in-memory value (not the attribute’s value in the database).

This dictionary is used in code line 2, which attempts to reapply the changes that failed to be committed. This approach is referred to as a “last write wins” approach because code line 2 commits the changes regardless of the values in the database. So if the optimistic locking failure was caused because another user or process changed the data in the database that corresponds to the edited enterprise object, code line 2 disregards those changes and writes the changes that failed in their place. This may or may not be a reasonable approach for your application.