Search code examples
iosmacoscore-datansfetchrequestnsnotification

Fetch request returns old data


I'm getting outdated results when executing a fetch request upon a NSManagedObjectContextObjectsDidChangeNotification.

As an example, consider directories with documents that can be deleted logically with a boolean attribute (softDeleted). The fetch request should return all directories that have at least one non-deleted document (directoriesWithDocuments).

The initial state is single directory with a single deleted document. directoriesWithDocuments returns an empty array.

The following code restores the document by setting the softDeleted boolean to NO.

[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:@"undelete"];
document.softDeleted = @(NO);
NSError *error;
BOOL success = [_context save:&error]; // This triggers the notification
[_context.undoManager endUndoGrouping];

The save triggers a NSManagedObjectContextObjectsDidChangeNotification. I expected directoriesWithDocuments to return the directory, but instead it still returns an empty array.

- (void)objectsDidChangeNotification:(NSNotification*)notification
{
    NSArray *objects = [self directoriesWithDocuments]; // Still empty!
}

Yet, if I execute directoriesWithDocuments after saving the context, either right away or in the next run loop, it returns the directory as expected.

This is the code of the fetch request:

- (NSArray*)directoriesWithDocuments
{
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ANY documents.softDeleted == NO"];
    NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Directory"];
    fetchRequest.predicate = predicate;
    fetchRequest.includesPendingChanges = YES; // Just in case
    NSError *error = nil;
    NSArray *objects = [_context executeFetchRequest:fetchRequest error:&error];
    return directories;
}

I suspect that the context has some kind of cache for fetch requests that is not cleared until the notification is handled. Is this how Core Data is expected to behave? Or am I doing something wrong?

Workaround

I'm currently delaying the execution of the fetch request like this:

- (void)objectsDidChangeNotification:(NSNotification*)notification
{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // HACK: Give time to Core Data to process pending changes or invalidate caches
        NSArray *objects = [self directoriesWithDocuments]; // Returns the directory as expected
    }];

}

Experiments

Per @MikePollard's suggestion, I checked the returned value of directoriesWithDocuments in NSManagedObjectContextWillSaveNotification and NSManagedObjectContextDidSaveNotification. The results are (in order):

  1. NSManagedObjectContextObjectsDidChangeNotification: empty (wrong)
  2. NSManagedObjectContextWillSaveNotification: empty (wrong)
  3. NSManagedObjectContextDidSaveNotification: 1 directory (correct)

Solution

  • It strikes me that the predicate is going to be translated into a SQL statement so until the save hits the DB you're not going to get the result you want ... (Not that'd you'd image that from just reading the NSFetchRequest documentation.)

    So try doing the fetch as a result of the NSManagedObjectContextDidSaveNotification.

    I bet if you turn on -com.apple.CoreData.SQLDebug 1 you'll see the SQL statement.