I have a NSFetchedResultsController
that fetches data from a CoreData store. When the frc is initialized I call performFetch
and check the number of fetched objects in frc.fetchedObjects
the result is 0 as it should be.
In another place in the code I call:
[obj.managedObjectContext refreshObject:obj mergeChanges:NO]
Although I'm not even sure it is needed it has the side effect of causing the frc to fetch some objects which were not fetched initially and shouldn't be fetched anyway considering the query used. These objects are exactly the same objects refreshObject:mergeChanges:
was called with.
Why is this happening?
Edit:
It doesn't happen for this query:
query.predicate = [NSPredicate predicateWithFormat:@"(cartEntry != NULL) AND (urlWeb = NULL)", version, dateUrlExpire];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
[frc performFetch:&error];
But when I change the query to this version then it happens (urlWebVersion
and urlWebDate
are NULL
for all records):
query.predicate = [NSPredicate predicateWithFormat:@"(cartEntry != NULL) AND ((urlWeb = NULL) OR (urlWebVersion != %@) OR (urlWebDate > %@))", version, dateUrlExpire];
* Edit #2 *
Here is a minimal code sample that shows the strange behavior (no error during execution):
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
NSError *error;
// Create an empty entity with the optional fields attr1 (string) and attr2 (date)
Entity *e = [NSEntityDescription insertNewObjectForEntityForName:@"Entity" inManagedObjectContext:[self managedObjectContext]];
// Save entity
[_managedObjectContext save:&error];
// Setup fetched results controller
NSPredicate *pred = [NSPredicate predicateWithFormat:@"(attr1 != %@) AND (attr2 != %@)", @"", [NSDate new], nil];
NSFetchRequest *query = [[NSFetchRequest alloc] initWithEntityName:@"Entity"];
query.predicate = pred;
query.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"attr1" ascending:NO]];
frc = [[NSFetchedResultsController alloc] initWithFetchRequest:query managedObjectContext:_managedObjectContext sectionNameKeyPath:nil cacheName:nil];
frc.delegate = self;
// Load data
[frc performFetch:&error];
// Output #1
NSLog(@"%ld", frc.fetchedObjects.count);
[_managedObjectContext refreshObject:e mergeChanges:NO];
return YES;
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(@"%ld", controller.fetchedObjects.count);
}
Output:
2015-01-22 22:53:44.293 CoreDataBug[10740:1445186] 0
2015-01-22 22:53:44.317 CoreDataBug[10740:1445186] 1
Your problem is that NSPredicate has difficulty matching !=
to a nil value. For example, in your Edit 2 your line:
[NSPredicate predicateWithFormat:@"(attr1 != %@) AND (attr2 != %@)",
@"", [NSDate new], nil];
as you know, returns zero after the fetch request. (There's an extra nil there, but it doesn't affect anything). But changing it to:
NSPredicate *pred = [NSPredicate predicateWithFormat:
@"((attr1 != %@) || (attr1 == nil)) AND ((attr2 != %@) || (attr2 == nil))",
@"", [NSDate new]];
does return the entities, even though logically these are the same.
This is documented behavior, albeit hard to follow for something so counter-intuitive, see "Using Null Values" subsection in Predicate Programming Guide.
It's unclear why refreshing the object would cause the predicate to suddenly match; this may be a bug. However, given the above documentation, !=
should not be used for values that might be nil (unless checking != nil
) without a paired || (x = nil)
. More than a little annoying and counter intuitive.