CoreData - Inconsistent behaviour when deleting objects on background context (attached test-project)

StackOverflow https://stackoverflow.com/questions/23633960

  •  21-07-2023
  •  | 
  •  

Question

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

Solution

In your recreateIssue method, you're fetching a parent object in the main context and obtaining one of its children, but then you're passing the child to deleteChild:, which deletes the child in the background context. You should be deleting the child in the same context its registered in.

Consider changing the following line of code...

CDIParent *parent = [self parentInContext:self.mainContext];

...like so...

CDIParent *parent = [self parentInContext:self.backgroundContext];

Here's what's happening, step by step:

  1. New managed objects are inserted in the child context and assigned temporary managed object IDs.

  2. The child context is saved. The save simply propagates the inserted objects to its parent context, but doesn't update the store. The temporary managed object IDs are unchanged.

  3. The parent context is saved, and its managed objects are assigned permanent managed object IDs. The managed objects IDs in the child context are still unchanged.

  4. Objects are obtained by managed object ID in the child context using permanent IDs from the parent context. This results in copies of the managed objects from the parent store being created in the child context. When the child context is saved, the deletions are propagated to the parent context. This has no effect on the original objects inserted in the child context, which still have temporary IDs.

  5. Finally, the main context is saved, propagating the deletions to the persistent store. The deleted objects no longer appear in the parent context, but the original managed objects inserted in the child context, still with their temporary IDs, are still there because they were never saved directly to the persistent store (instead their changes were merged with the parent context), and the context was never reset.

Solution

Either fetch the objects into the child store once they've been persisted, (as shown above), or simply call reset on the child context anytime after the parent context has been saved, as show below:

[self.backgroundContext reset];

By the way, I noticed that your custom logging implementation was obscuring vital details, in particular by masking differences between temporary and permanent managed object IDs. Inspecting the context's registeredObjects array in the debugger made those differences immediately apparent. You might be better off simply passing the array directly to NSLog, rather than using custom code to describe the objects.

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