Question

I'm doing operations in a GCD dispatch queue on a NSManagedObjectContext defined like this:

- (NSManagedObjectContext *)backgroundContext
{
    if (backgroundContext == nil) {
        self.backgroundContext = [NSManagedObjectContext MR_contextThatNotifiesDefaultContextOnMainThread];
    }
    return backgroundContext;
}

MR_contextThatNotifiesDefaultContextOnMainThread is a method from MagicalRecord:

NSManagedObjectContext *context = [[self alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[context setParentContext:[NSManagedObjectContext MR_defaultContext]];
return context;

After fetching my objects and giving them the correct queue position i log them and the order is correct. However, the second log seems to be completely random, the sort descriptor clearly isn't working.

I have narrowed down the Problem to [self.backgroundContext save:&error]. After saving the background context sort descriptors are broken.

dispatch_group_async(backgroundGroup, backgroundQueue, ^{
    // ...

    for (FooObject *obj in fetchedObjects) {
        // ...
        obj.queuePosition = [NSNumber numberWithInteger:newQueuePosition++];
    }

    NSFetchRequest *f = [NSFetchRequest fetchRequestWithEntityName:[FooObject entityName]];
    f.predicate = [NSPredicate predicateWithFormat:@"queuePosition > 0"];
    f.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:@"queuePosition" ascending:YES]];
    NSArray *queuedObjects = [self.backgroundContext executeFetchRequest:f error:nil];
    for (FooObject *obj in queuedObjects) {
        DLog(@"%@ %@", obj.queuePosition, obj.title);
    }

    if ([self.backgroundContext hasChanges]) {
        DLog(@"Changes");
        NSError *error = nil;
        if ([self.backgroundContext save:&error] == NO) {
            DLog(@"Error: %@", error);
        }
    }

    queuedObjects = [self.backgroundContext executeFetchRequest:f error:nil];
    for (FooObject *obj in queuedObjects) {
        DLog(@"%@ %@", obj.queuePosition, obj.title);
    }

});

I've got no idea why the sort descriptor isn't working, any Core Data experts want to help out?

Update:

The problem does not occur on iOS 4. I think the reason is somewhere in the difference between thread isolation and private queue modes. MagicalRecord automatically uses the new concurrency pattern which seems to behave differently.

Update 2:

The problem has been solved by adding a save of the background context:

if ([[NSManagedObjectContext MR_contextForCurrentThread] hasChanges]) {
    DLog(@"Changes");
    NSError *error = nil;
    if ([[NSManagedObjectContext MR_contextForCurrentThread] save:&error] == NO) {
        DLog(@"Error: %@", error);
    } else {
        NSManagedObjectContext *parent = [NSManagedObjectContext MR_contextForCurrentThread].parentContext;
        [parent performBlockAndWait:^{
            NSError *error = nil;
            if ([parent save:&error] == NO) {
                DLog(@"Error saving parent context: %@", error);
            }
        }];
    }
}

Update 3:

MagicalRecord offers a method to recursively save a context, now my code looks like this:

if ([[NSManagedObjectContext MR_contextForCurrentThread] hasChanges]) {
    DLog(@"Changes");
    [[NSManagedObjectContext MR_contextForCurrentThread] MR_saveWithErrorHandler:^(NSError *error) {
        DLog(@"Error saving context: %@", error);
    }];
}

Shame on me for not using it in the first place...

However, I don't know why this helps and would love to get an explanation.

Was it helpful?

Solution

I'll try to comment, since I wrote MagicalRecord.

So, on iOS5, MagicalRecord is set up to try to use the new Private Queue method of multiple managed object contexts. This means that a save in the child context only pushes saves up to the parent. Only when a parent with no more parents saves, does the save persist to its store. This is probably what was happening in your version of MagicalRecord.

MagicalRecord has tried to handle this for you in the later versions. That is, it would try to pick between private queue mode and thread isolation mode. As you found out, that doesn't work too well. The only truly compatible way to write code (without complex preprocessor rules, etc) for iOS4 AND iOS5 is to use the classic thread isolation mode. MagicalRecord from the 1.8.3 tag supports that, and should work for both. From 2.0, it'll be only private queues from here on in.

And, if you look in the MR_save method, you'll see that it's also performing the hasChanges check for you (which may also be unneeded since the Core Data internals can handle that too). Anyhow, less code you should have to write and maintain...

OTHER TIPS

The actual underlying reason why your original setup didn't work is an Apple bug when fetching from a child context with sort descriptors when the parent context is not yet saved to store:

NSSortdescriptor ineffective on fetch result from NSManagedContext

If there is any way you can avoid nested contexts, do avoid them as they are still extremely buggy and you will likely be disappointed with the supposed performance gains, cf. also: http://wbyoung.tumblr.com/post/27851725562/core-data-growing-pains

Since CoreData isnot a safe-thread framework and for each thread(operation queue), core data uses difference contexts. Please refer the following excellent writing

http://www.duckrowing.com/2010/03/11/using-core-data-on-multiple-threads/

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