stackunderflow
stackunderflow

Reputation: 892

Why might Core Data fail to merge data from private context to main context?

I have an application using Core Data with the following, fairly standard, managed object context hierarchy:

Persistent Store Coordinator 
    ↳ Save Context (Private Queue Concurrency Type) 
        ↳ Main Context (Main Queue Concurrency Type)
        ↳ Private Context (Private Queue Concurrency Type)

The merge policy for all managed object contexts is set to NSMergeByPropertyObjectTrumpMergePolicy

I am observing NSManagedObjectContextDidSaveNotification which will invoke the following function when the Private Context is saved and merge changes to the Main Context:

func contextDidSaveNotificationHandler(notification: NSNotification) {

    if let savedContext = notification.object as? NSManagedObjectContext {
        if savedContext == privateObjectContext {

            mainObjectContext.performBlock({

                if let updatedObjects = notification.userInfo![NSUpdatedObjectsKey] as? Set<NSManagedObject> {
                    //
                    // fire faults on the updated objects
                    //
                    for obj in updatedObjects {
                        mainObjectContext.objectWithID(obj.objectID).willAccessValueForKey(nil)
                    }
                }

                mainObjectContext.mergeChangesFromContextDidSaveNotification(notification)
            })
        }
    }
}

This is working most of the time but sometimes I am finding that changes to existing objects in the Private Context are not being merged into the Main Context. I can't figure out why -- the private context save is successful; the NSManagedObjectContextDidSaveNotification notification is being sent; the notification handler is being invoked; notification.userInfo?[NSUpdatedObjectsKey] contains the correctly updated objects; but at the end, the main context is not synchronized with the private context. (ie: the managed objects in the main context are not in sync with the values contained in notification.userInfo?[NSUpdatedObjectsKey]) If I kill the app and relaunch it, the contexts become synchronized again (after loading objects from the persistent store).

I have -com.apple.CoreData.ConcurrencyDebug 1 enabled in my launch arguments, and all Core Data multithreading rules are being followed. I can't see anything overtly wrong with my managed object context hierarchy or the merging function. What could possibly be causing this?

Upvotes: 8

Views: 2716

Answers (4)

backslash-f
backslash-f

Reputation: 8193

I used to use a similar structure as yours, but it wasn't reliable in my case. Sometimes it did work, sometimes it didn't. One of the errors was "incomplete merges", just like you described. I started observing this behavior in iOS 10. I believe something may have change on Core Data's core.

Anyway, I changed my approach. I started using the Apple's sample code on Core Data / Concurrency at:

https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/CoreData/Concurrency.html#//apple_ref/doc/uid/TP40001075-CH24-SW3

If you read the entire page (it isn't that big), you may notice that they suggest creating a private queue, as usual, but then:

This example can be further simplified when using an NSPersistentContainer:

let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = EmployeeMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

Of course I'm not sure if the above code fits in your requirements exactly. However the key thing there is to rely on the persistentContainer to do the heavy lifting.

After all of the data has been consumed and turned into NSManagedObject instances, you call save on the private context, which moves all of the changes into the main queue context without blocking the main queue.

Upvotes: 1

Jacob Boyd
Jacob Boyd

Reputation: 690

You could always set your mainQueueContext as your privateQueueContext's parent:

privateObjectContext.parent = mainObjectContext

This will merge your saves into your main object context. I used this in a project where I parse JSON, and set core data objects in a privateQueue's perform block, with my mainContext set as the parent. Then I simple save the private, and then access the data from the main thread/main context. Works like a charm. I should add I do not keep the private context in memory, it is created new when I need it.

Upvotes: 0

Jon Rose
Jon Rose

Reputation: 8563

You can't merge sibling context. You have to merge from the parent down. In other words change

 if savedContext == privateObjectContext {

to

 if savedContext == savingObjectContext {

where the savingObjectContext is the parent context of the main context.

Upvotes: 0

Dale
Dale

Reputation: 3323

The problem is with your merge policy. Try changing to to NSErrorMergePolicy and I think you will start to see merge conflicts. At the least this will give you more of an idea of the underlying cause

Upvotes: -1

Related Questions