We're experiencing odd behaviour in one of our apps related to removing objects with multiple contexts.
After removing an object on the background context, it still exists in the relationship from its parent.
The error occurs when deleting objects fetched using existingObjectWithID, but NOT when using objectWithID or through executeFetchRequest.
However, since documentation suggests that existingObjectWithID is a safer method to use, we'd rather not change it and potentially introduce crashes elsewhere.
Example
In the output below, 5 children objects are created and then removed one by one.
Setup
Children by fetchRequest in mainContext: 5
Children by fetchRequest in backgroundContext: 5
Children by parent relationship in mainContext: 5
Children by parent relationship in backgroundContext: 5
Parent on mainContext: {
children = (
"93139831-EAC9-46AF-9B93-7AFBCAA3C380",
"19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
"73082A38-ECC3-45FA-995E-3ADD46671A46",
"6D7752E3-44BF-4418-A9DD-607896167510",
"CB325763-E340-4FF2-96E8-67206794C91B"
);
id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}
Parent on backgroundContext: {
children = (
"19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
"6D7752E3-44BF-4418-A9DD-607896167510",
"73082A38-ECC3-45FA-995E-3ADD46671A46",
"CB325763-E340-4FF2-96E8-67206794C91B",
"93139831-EAC9-46AF-9B93-7AFBCAA3C380"
);
id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}
Deleting children
*** Deleted child with ID: 93139831-EAC9-46AF-9B93-7AFBCAA3C380
*** Deleted child with ID: 19E51ADE-4524-4285-9DF3-4B0DDE58FAA2
*** Deleted child with ID: 73082A38-ECC3-45FA-995E-3ADD46671A46
*** Deleted child with ID: 6D7752E3-44BF-4418-A9DD-607896167510
*** Deleted child with ID: CB325763-E340-4FF2-96E8-67206794C91B
After deletions
Children by fetchRequest in mainContext: 0
Children by fetchRequest in backgroundContext: 0
Children by parent relationship in mainContext: 0
Children by parent relationship in backgroundContext: 5
Parent on mainContext: {
children = (
);
id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}
Parent on backgroundContext: {
children = (
"19E51ADE-4524-4285-9DF3-4B0DDE58FAA2",
"6D7752E3-44BF-4418-A9DD-607896167510",
"73082A38-ECC3-45FA-995E-3ADD46671A46",
"CB325763-E340-4FF2-96E8-67206794C91B",
"93139831-EAC9-46AF-9B93-7AFBCAA3C380"
);
id = "69B4180C-91BB-4B4D-8F01-B1612C7B6B0E";
}
How is it possible for the CDIParent on the background context to retain its children, while fetching CDIChild on the same context returns none?
After deletions using objectWithID instead
Children by fetchRequest in mainContext: 0
Children by fetchRequest in backgroundContext: 0
Children by parent relationship in mainContext: 0
Children by parent relationship in backgroundContext: 0
Parent on mainContext: {
children = (
);
id = "4C812CAB-4075-4A5C-9150-FDAEB4A6D238";
}
Parent on backgroundContext: {
children = (
);
id = "4C812CAB-4075-4A5C-9150-FDAEB4A6D238";
}
For now, we use executeFetchRequest as a work around, but the problem suggests that we have a fundamental problem with our CoreData setup.
Test project
I've created a test app for debugging this issue, it can be downloaded here:
https://dl.dropboxusercontent.com/u/29710262/StackOverflow/CoreDataIssue.zip
Main Source Code
//
// AppDelegate.m
// CoreDataIssue
//
#import "AppDelegate.h"
#import "CDIParent.h"
#import "CDIChild.h"
#import <CoreData/CoreData.h>
//-----------------------------------------------------------------
@implementation AppDelegate
//-----------------------------------------------------------------
//-----------------------------------------------------------------
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
//-----------------------------------------------------------------
{
[self initCoreData];
[self initObjects];
[self recreateIssue];
return YES;
}
#pragma mark - Private
//-----------------------------------------------------------------
- (void)initObjects;
//-----------------------------------------------------------------
{
__block NSError *error = nil;
// Create 5 entities on backgroundContext
[self.backgroundContext performBlockAndWait:^{
CDIParent *parent = [CDIParent parentInContext:self.backgroundContext error:&error];
for (NSUInteger i = 0; i < 5; i++) {
[CDIChild childInParent:parent error:&error];
}
// Save contexts
[self saveContext:self.backgroundContext];
[self.mainContext performBlockAndWait:^{
[self saveContext:self.mainContext];
}];
}];
[self debugChildrenWithComment:@"Created objects"];
}
//-----------------------------------------------------------------
- (void)recreateIssue;
//-----------------------------------------------------------------
{
[self debugParents];
// Remove all entities
CDIParent *parent = [self parentInContext:self.mainContext];
while (parent.children.count > 0) {
[self deleteChild:parent.children.allObjects.firstObject];
}
[self debugParents];
}
//-----------------------------------------------------------------
- (void)deleteChild:(CDIChild *)child;
//-----------------------------------------------------------------
{
__block NSError *error = nil;
NSString *logID = child.childID;
NSManagedObjectID *objectID = child.objectID;
// Remove on backgroundContext
[self.backgroundContext performBlockAndWait:^{
// Lookup child in backgroundContext
CDIChild *object = (CDIChild *) [self.backgroundContext existingObjectWithID:objectID error:&error];
// Delete child
[self.backgroundContext deleteObject:object];
// Save contexts
[self saveContext:self.backgroundContext];
[self.mainContext performBlockAndWait:^{
[self saveContext:self.mainContext];
}];
}];
[self debugChildrenWithComment:[NSString stringWithFormat:@"Deleted child with ID: %@", logID]];
}
//-----------------------------------------------------------------
- (CDIParent *)parentInContext:(NSManagedObjectContext *)context;
//-----------------------------------------------------------------
{
NSError *error = nil;
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"];
CDIParent *parent = [context executeFetchRequest:fetchRequest error:&error].firstObject;
if (error != nil) {
NSLog(@"Error: %@", error);
}
return parent;
}
//-----------------------------------------------------------------
- (void)debugChildrenWithComment:(NSString *)comment;
//-----------------------------------------------------------------
{
NSLog(@"*** %@", comment);
NSError *error = nil;
NSFetchRequest *fetchRequest = nil;
// First, log children by fetch request
fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Child"];
NSLog(@"Children by fetchRequest in mainContext: %lu", (unsigned long) [self.mainContext countForFetchRequest:fetchRequest error:&error]);
NSLog(@"Children by fetchRequest in backgroundContext: %lu", (unsigned long) [self.backgroundContext countForFetchRequest:fetchRequest error:&error]);
// Second, log children by relationship
fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Parent"];
{
CDIParent *parent = (CDIParent *) [self.mainContext executeFetchRequest:fetchRequest error:&error].firstObject;
NSLog(@"Children by parent relationship in mainContext: %lu", (unsigned long) parent.children.count);
}
{
CDIParent *parent = (CDIParent *) [self.backgroundContext executeFetchRequest:fetchRequest error:&error].firstObject;
NSLog(@"Children by parent relationship in backgroundContext: %lu", (unsigned long) parent.children.count);
}
if (error != nil) {
NSLog(@"Error: %@", error);
}
NSLog(@"\n");
}
//-----------------------------------------------------------------
- (void)debugParents;
//-----------------------------------------------------------------
{
NSLog(@"Parent on mainContext: %@", [[self parentInContext:self.mainContext] log]);
NSLog(@"Parent on backgroundContext: %@", [[self parentInContext:self.backgroundContext] log]);
}
#pragma mark - Core Data
//-----------------------------------------------------------------
- (void)initCoreData;
//-----------------------------------------------------------------
{
NSError *error = nil;
// Create Model
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CoreDataIssue" withExtension:@"momd"];
self.managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
// Create Persistent Store Coordinate
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"CoreDataIssue.sqlite"];
if ([[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error] == NO) {
NSLog(@"Error while removing store: %@", error);
}
self.persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![self.persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
// Create Contexts
self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[self.mainContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
[self.mainContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[self.backgroundContext setParentContext:self.mainContext];
[self.backgroundContext setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[self debugChildrenWithComment:@"Core Data initialized"];
}
//-----------------------------------------------------------------
- (void)saveContext:(NSManagedObjectContext *)managedObjectContext;
//-----------------------------------------------------------------
{
NSError *error = nil;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
}
}
#pragma mark - Application's Documents directory
//-----------------------------------------------------------------
- (NSURL *)applicationDocumentsDirectory;
//-----------------------------------------------------------------
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
//-----------------------------------------------------------------
@end