Question

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)
Was it helpful?

Solution 2

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.

OTHER TIPS

First of all, it looks like you have a boolean attribute called "deleted" defined in your model.

I recall that doing so can be a significant problem because it conflicts (at a KVC level) with NSManagedObject isDeleted. You might want to change that just to make sure it's not the culprit.

Edit

Thanks for replying. I used deleted as a simple example; it's not the actual attribute I'm using. Will change it to softDeleted to avoid confusion

I've updated my suggestion below to match the softDeleted in your example.


That said, I think what's at work here boils down to the question of what constitutes a "change" for includesPendingChanges = YES.

Before the context has completed its save, the only 'change' is to Document entities, not Directory entities.

So when the fetch request includes pending changes, there are no Directory entities with any pending changes so you end up with the previous results.

To test that theory, give this a shot:

[_context.undoManager beginUndoGrouping];
[_context.undoManager setActionName:@"delete"];
document.softDeleted = @(NO);
[document.directory willChangeValueForKey:@"documents"] // any attribute will do really
[document.directory didChangeValueForKey:@"documents"]
NSError *error;
BOOL success = [_context save:&error];
[_context.undoManager endUndoGrouping];

What you're doing with the fake will/did changeValueForKey is to "dirty" the associated Directory object. My guess is that it will then be considered "changed" and, as such, included the fetch results.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top