30
votes

I have an app with two managed object contexts setup like this:

  • Parent Context: NSPrivateQueueConcurrencyType, linked to the persistent store.
  • Main Context: NSMainQueueConcurrencyType, child of Parent Context.

When insert a new managed object to the main context, I save the main context and then the parent context like this:

[context performBlockAndWait:^{
    NSError * error = nil;
    if (![context save: &error]) {
        NSLog(@"Core Data save error %@, %@", error, [error userInfo]);
    }
}];

[parentContext performBlock:^{
    NSError *error = nil;
    BOOL result = [parentContext save: &error];
    if ( ! result ) {
        NSLog( @"Core Data save error in parent context %@, %@", error, [error userInfo] );
    }
}];

My understanding is that when the manage object is first created, it has a temporary objectID. Then the main context is saved and this object, with its temporary ID, gets to the parent context. Then the parent context is saved. When this last context is saved, the temporary objectID in the parent context gets transformed into a permanent objectID.

So:

  • Does the permanent object ID ever get propagated automatically back to the main (child) context?
  • When I force to get the object permanent ID with [NSManagedObjectContext obtainPermanentIDsForObjects:error:], then background the app, reactivate it, reload, get the object using main context's objectWithID:, and access a property, I get

    "CoreData could not fulfill a fault for ...".

What is wrong with this approach?
3
Jorge: I'm getting a CoreData could not fulfill a fault when obtaining a permanent ID in a child context & saving the ManagedObject. Did you found out what was the reason ?. Thanksthe Reverend

3 Answers

43
votes

It is a known bug, hopefully fixed soon, but in general, obtaining a permanent ID is sufficient, provided you do so before you save the data in the first child, and you only include the inserted objects:

[moc obtainPermanentIDsForObjects:moc.insertedObjects.allObjects error:&error]

In some complex cases, it is better to get a permanent ID as soon as you create the instance, especially if you have complex relationships.

How and when are you calling obtainPermanentIDsForObjects?

I do not follow the part about the app crashing. Maybe a better explanation would help.

11
votes

As Jody said above, when creating a new NSManagedObject in a background thread using a child ManagedObjectContext you must force the creation of a permanent ID by doing the following BEFORE you save:

NSError *error = nil;

[threadedMOC obtainPermanentIDsForObjects:threadedMOC.insertedObjects.allObjects error:&error];

BOOL success = [threadedMOC save:&error];

IMHO, it's not really intuitive to do it this way - after all, you're asking for a permanent ID BEFORE you save! But this is the way it seems to work. If you ask for the permanent ID after the save, then the ID will still be temporary. From the Apple Docs, you can actually use the following to determine if the object's ID is temporary:

BOOL isTemporary = [[managedObject objectID] isTemporaryID];
2
votes

Problem still exists in iOS 8.3 Solution in Swift :

func saveContext(context: NSManagedObjectContext?){
   NSOperationQueue.mainQueue().addOperationWithBlock(){
    if let moc = context {
        var error : NSError? = nil
        if !moc.obtainPermanentIDsForObjects(Array(moc.insertedObjects), error: &error){
            println("\(__FUNCTION__)\n \(error?.localizedDescription)\n \(error?.userInfo)")
        }
        if moc.hasChanges && !moc.save(&error){
            println("\(__FUNCTION__)\n \(error?.localizedDescription)\n \(error?.userInfo)")
        }
    }
 }
}

func saveBackgroundContext(){
    saveContext(self.defaultContext)

    privateContext?.performBlock{
        var error : NSError? = nil
        if let context = self.privateContext {

            if context.hasChanges && !context.save(&error){
                println("\(__FUNCTION__)\n \(error?.localizedDescription)\n \(error?.userInfo)")
            }else {
                println("saved private context to disk")
            }
        }
    }
}

Where:

  • defaultContext has concurrencyType .MainQueueConcurrencyType
  • privateContext has concurrencyType .PrivateQueueConcurrencyType