Change Management

If your application contains more than one managed object context and you allow objects to be modified in more than context, then you need to be able to reconcile the changes.

Disjoint Edits

The object graph associated with any given managed object context must be internally consistent. If you have multiple managed object contexts in the same application, however, it is possible that each may contain objects that represent the same records in the persistent store, but whose characteristics are mutually inconsistent. In an employee application, for example, you might have two separate windows that display the same set of employees, but distributed between different departments and with different managers, as shown in Figure 1.

Figure 1  Managed object contexts with mutually inconsistent data values
Managed object contexts with mutually inconsistent data values

Ultimately though there can only be one “truth” and differences between these views must be detected and reconciled when data is saved. When one of the managed object contexts is saved, its changes are pushed through the persistent store coordinator to the persistent store. When the second managed object context is saved, conflicts are detected using a mechanism called optimistic locking; how the conflicts are resolved depends on how you have configured the context.

Conflict Detection and Optimistic Locking

When Core Data fetches an object from a persistent store, it takes a snapshot of its state. A snapshot is a dictionary of an object’s persistent properties—typically all its attributes and the global IDs of any objects to which it has a to-one relationship. Snapshots participate in optimistic locking. When the framework saves, it compares the values in each edited object’s snapshot with the then-current corresponding values in the persistent store.

  • If the values are the same, then the store has not been changed since the object was fetched, so the save proceeds normally. As part of the save operation, the snapshots' values are updated to match the saved data.

  • If the values differ, then the store has been changed since the object was fetched or last saved; this represents an optimistic locking failure.

Conflict Resolution

You can get an optimistic locking failure if more than one persistence stack references the same external data store (whether you have multiple persistence stacks in a single application or you have multiple applications). In this situation there is the possibility that the same conceptual managed object will be edited in two persistence stacks simultaneously. In many cases, you want to ensure that subsequent changes made by the second stack do not overwrite changes made by the first, but there are other behaviors that may be appropriate. You can choose the behavior by choosing for the managed object context a merge policy that is suitable for your situation.

The default behavior is defined by the NSErrorMergePolicy. This policy causes a save to fail if there are any merge conflicts. In the case of failure, the save method returns with an error with a userInfo dictionary that contains the key @"conflictList"; the corresponding value is an array of conflict records. You can use the array to tell the user what differences there are between the values they are trying to save and those current in the store. Before you can save you must either fix the conflicts (by re-fetching objects so that the snapshots are updated) or choose a different policy. The NSErrorMergePolicy is the only policy that generates an error. Other policies—NSMergeByPropertyStoreTrumpMergePolicy, NSMergeByPropertyObjectTrumpMergePolicy, and NSOverwriteMergePolicy—allow the save to proceed by merging the state of the edited objects with the state of the objects in the store in different ways. The NSRollbackMergePolicy discards in-memory state changes for objects in conflict and uses the persistent store’s version of the objects’ state.

Snapshot Management

An application that fetches hundreds of rows of data can build up a large cache of snapshots. Theoretically, if enough fetches are performed, a Core Data-based application can contain all the contents of a store in memory. Clearly, snapshots must be managed in order to prevent this situation.

Responsibility for cleaning up snapshots rests with a mechanism called snapshot reference counting. This mechanism keeps track of the managed objects that are associated with a particular snapshot—that is, managed objects that contain data from a particular snapshot. When there are no remaining managed object instances associated with a particular snapshot (which Core Data determines by maintaining a list of these references), the strong reference to the snapshot is broken.

Communicating Changes Between Contexts

If you use more than one managed object context in an application, Core Data does not automatically notify one context of changes made to objects in another. In general, this is because a context is intended to provide a scratch pad where you can make changes to objects in isolation, and if you wish you can discard the changes without affecting other contexts. If you do need to synchronize changes between contexts, how a change should be handled depends on the user visible semantics you want in the second context, and on the state of the objects in the second context.

Consider an application with two managed object contexts and a single persistent store coordinator. If a user deletes an object in the first context (moc1), you may need to inform the second context (moc2) that has been deleted. In all cases, moc1 posts an NSManagedObjectContextDidSave notification that your application should register for and use as the trigger for whatever actions it needs to take. This notification contains information not only about deleted objects, but also about changed objects. You need to handle these changes since they may be the result of the delete (most of the ways this can happen involve transient relationships or fetched properties).

There are multiple axes you must consider when deciding how you want to handle your delete notification. The important ones are:

These are somewhat orthogonal, and what actions you take to synchronize the contexts depend on the semantics of your application. The following three strategies are presented in order of increasing complexity.

  1. The simplest case is when the object itself has not changed in moc2 and you do not have to worry about undo; in this situation, you can just delete the object. The next time moc2 saves, the framework will notice that you are trying to re-delete an object, ignore the optimistic locking warning, and continue without error.

  2. If you do not care about the contents of moc2, you can simply reset it (using reset) and refetch any data you need after the reset. This will reset the undo stack as well, and the deleted object is now gone. The only issue here is determining what data to refetch. You can do this by, before you reset, collecting the IDs (objectID) of the managed objects you still need and using those to reload once the reset has happened (you must exclude the deleted IDs, and it is best to create fetch requests with IN predicates to avoid problems will not being able to fulfill faults for deleted IDs).

  3. If the object has changed in moc2, but you do not care about undo, your strategy depends on what it means for the semantics of your application. If the object that was deleted in moc1 has changes in moc2, should it be deleted from moc2 as well? Or should it be resurrected and the changes saved? What happens if the original deletion triggered a cascade delete for objects that have not been faulted into moc2? What if the object was deleted as part of a cascade delete?

    There are two workable options (a third, unsatisfactory option is described later):

    1. The simplest strategy is to just discard the changes by deleting the object.

    2. Alternatively, if the object is standalone, you can set the merge policy on the context to NSMergePolicyOverwrite. This will cause the changes in the second context to overwrite the delete in the database.

      Note that this will cause all changes in moc2 to overwrite any changes made in moc1.

The preceding are the best solutions, and are least likely to leave your object graph in an unsustainable state as a result of something you missed. There are various other strategies, but all are likely to lead to inconsistencies and errors. They are listed here as examples so that you can recognize them and avoid them. If you find yourself trying to adopt any of these strategies, you should redesign your application's architecture to follow one of the patterns described previously.

  1. If you have a situation like 3(b) above, but the object not standalone, and for some reason you want to save those changes, the best you're likely to be able to do is to resurrect the part of the graph that had been loaded into moc2, which may or may not make sense in the context of your application. Again you do this by setting the merge policy to NSMergePolicyOverwrite, but you also need some up-front application design, and some meddling with the objects in the 'deleted' object's relationships.

    In order for the world to make some amount of sense later, you need to automatically fault in any relationships that might need to be resurrected when you fault in the object. Then, when you get a delete notification, you need to make the context think all the objects related to the deleted object have changed, so that they will be saved as well. This will bloat your application's memory use, since you'll end up with possibly irrelevant data as a precaution against something that may not happen, and if you're not careful, you can end up with your database in a hybrid state where it is neither what moc1 tried to create, nor what moc2 would expect (for example, if you missed a relationship somewhere and you now have partial relationships, or orphaned nodes).

  2. The second worst of all worlds is when you have changes to other objects you can't blow away in the second MOC, the object itself has changes that you are willing to discard, and you care about undo. You can't reset the context, because that loses the changes. If you delete the object, the delete will get pushed onto the undo stack and will be undoable, so the user could undo, resave, and run into the semantic problems mentioned in 3 above, only worse because you have not planned for them.

    The only real way to solve this is to—separately, in your application code—keep track of the objects which are changed as a result of the delete. You then need to track user undo events, and when the user undoes past a delete, you can then "rerun" the deletion. This is likely to be complex and inefficient if a significant number of changes are propagated.

  3. The worst case is you have changes to other objects you cannot discard, the object has changes you want to keep, and you care about undo. There may be a way to deal with this, but it will require considerable effort and any solution is likely to be complicated and fragile.