Core Data Gotcha

Today I want to talk about how two individually reasonable design decisions in Core Data can combine to bite the unwary programmer. I’ve mentioned the benefits of the NSFetchedResultsController class’s ability to monitor the objects in its MOC for changes, and discussed the usefulness of the NSManagedObjectContext class’s mergeChangesFromContextDidSaveNotification: method. When these two features are combined with Core Data faults, however, the results may be undesirable.

Faults

In Core Data, faults are placeholders, or “unrealized objects”. They are small objects which refer to other NSManagedObjects, which are fetched into memory only as needed. This faulting mechanism is designed to enhance performance and reduce memory use.

In general, the faulting mechanism is transparent; when you retrieve an object from an NSManagedObjectContext (MOC) you can’t tell (in the normal course of its use) whether it’s a fault or a realized object. A fault will be converted into a realized object (“fired”) automatically by the Core Data framework in most cases when it is necessary to do so, e.g. when accessing a property of the object. If you need to fire a fault yourself (and, in this article, we’ll cover a case where you’ll need to) you can do so by invoking its willAccessValueForKey: method with a nil argument.

NSManagedObjectContext

Normally, MOCs are independent of one another; this gives them a good deal of their power. This can be problematic, however, when changes are committed to the persistent store in one MOC; at that point, one often wishes to propagate such changes to other MOCs. MOCs offer the mergeChangesFromContextDidSaveNotification: method, which takes the notification posted by a MOC after a commit operation, and:

… refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject: on those which have been deleted.

The term “refreshes” here is significant. Faults are not affected by this method, since the objects they reference aren’t held in memory, and references themselves normally wouldn’t be affected by updates to their underlying objects. Objects that aren’t currently represented in the MOC at all are, of course, not updated either.

NSFetchedResultsController

When a delegate is assigned to an NSFetchedResultsController, that delegate’s methods will be invoked when the objects in the controller’s results set change. Specifically:

If you set a delegate for a fetched results controller, the controller registers to receive change notifications from its managed object context. Any change in the context that affects the result set or section information is processed and the results are updated accordingly. The controller notifies the delegate when result objects change location or when sections are modified …

The significant phrase here is “any change in the context”. As we just saw, merged updates to faulted objects do not result in changes to a context. Neither do merged updates to objects that are not registered with a MOC.

Problems

Let me quickly point out that all this behavior is eminently reasonable; it makes sense to assume that changes to objects that haven’t been fully realized in a MOC can be ignored, and it makes sense to assume that unrealized objects can’t be changed s.t. a result set is altered. Both are usually true, just not in the case we’re about to consider.

Let’s suppose that you’ve created a request for all Author records in which the lastName field begins with the letter “W”, and populated an NSFetchedResultsController with this request. Let’s also assume that you haven’t referenced any of the records in the MOC in which the request was made (i.e. they are all, at best, faults). If you create a second MOC, and make and save changes to the Author objects within it, two things can go wrong:

  • You might change an Author record s.t. it should now be included in the set. (E.g. editing the record for “Douglas Adams” to refer to “Milton Wadams”.) This change will not be merged into the first MOC because the original “Douglas Adams” Author record is (at best) a fault; therefore the NSFetchedResultsController will not gain a reference to that record, which will not be displayed even though it meets the criteria of the original request.
  • You might change an Author record s.t. it should now be excluded from the set. (E.g. editing the record for “Milton Wadams” to refer to “Douglas Adams”.) This change will not be merged into the first MOC because the original “Milton Wadams” Author record is (at best) a fault; therefore the NSFetchedResultsController will still contain a reference to that record, which will eventually be displayed even though it no longer meets the criteria of the original request.

Note that, in both cases, the records will eventually be displayed with the correct information, it’s their membership (or lack thereof) in the results set that is not updated correctly.

Workaround

The key is to fault in updated records before doing a merge. Instead of registering one MOC to directly observe another for NSManagedObjectContextDidSaveNotifications, register a view controller to do the observing (with, e.g., a selector of mergeHelper:) and write code like this:

- (void)mergeHelper:(NSNotification*)saveNotification
{
	// Fault in all updated objects
	NSArray* updates = [[saveNotification.userInfo objectForKey:@"updated"] allObjects];
	for (NSInteger i = [updates count]-1; i >= 0; i--)
	{
		[[managedObjectContext objectWithID:[[updates objectAtIndex:i] objectID]] willAccessValueForKey:nil];
	}
		
	// Merge
	[managedObjectContext mergeChangesFromContextDidSaveNotification:saveNotification];
}
Share and Enjoy:
  • Twitter
  • Facebook
  • Digg
  • Reddit
  • HackerNews
  • del.icio.us
  • Google Bookmarks
  • Slashdot
This entry was posted in iPhone. Bookmark the permalink.

Comments are closed.