Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

NSPersistentContainer concurrency for saving to core data

I've read some blogs on this but I'm still confused on how to use NSPersistentContainer performBackgroundTask to create an entity and save it. After creating an instance by calling convenience method init(context moc: NSManagedObjectContext) in performBackgroundTask() { (moc) in } block if I check container.viewContext.hasChanges this returns false and says there's nothing to save, if I call save on moc (background MOC created for this block) I get errors like this:

fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
    "NSMergeConflict (0x17466c500) for NSManagedObject (0x1702cd3c0) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ... }fatal error: Failure to save context: Error Domain=NSCocoaErrorDomain Code=133020 "Could not merge changes." UserInfo={conflictList=(
    "NSMergeConflict (0x170664b80) for NSManagedObject (0x1742cb980) with objectID '0xd000000000100000 <x-coredata://3EE6E11B-1901-47B5-9931-3C95D6513974/Currency/p4>' with oldVersion = 1 and newVersion = 2 and old cached row = {id = 2; ...} and new database row = {id = 2; ...}"
)}

So I've failed to get the concurrency working and would really appreciate if someone could explain to me the correct way of using this feature on core data in iOS 10

like image 895
MFA Avatar asked Mar 11 '17 09:03

MFA


1 Answers

TL:DR: Your problem is that you are writing using both the viewContext and with background contexts. You should only write to core-data in one synchronous way.

Full explanation: If an object is changed at the same time from two different contexts core-data doesn't know what to do. You can set a mergePolicy to set which change should win, but that really isn't a good solution, because you can lose data that way. The way that a lot of pros have been dealing with the problem for a long time was to have an operation queue to queue the writes so there is only one write going on at a time, and have another context on the main thread only for reads. This way you never get any merge conflicts. (see https://vimeo.com/89370886#t=4223s for a great explanation on this setup).

Making this setup with NSPersistentContainer is very easy. In your core-data manager create a NSOperationQueue

//obj-c
_persistentContainerQueue = [[NSOperationQueue alloc] init];
_persistentContainerQueue.maxConcurrentOperationCount = 1;

//swift
let persistentContainerQueue = OperationQueue()
persistentContainerQueue.maxConcurrentOperationCount = 1

And do all writing using this queue:

// obj c
- (void)enqueueCoreDataBlock:(void (^)(NSManagedObjectContext* context))block{
  void (^blockCopy)(NSManagedObjectContext*) = [block copy];
    
  [self.persistentContainerQueue addOperation:[NSBlockOperation blockOperationWithBlock:^{
    NSManagedObjectContext* context = self.persistentContainer.newBackgroundContext;
    [context performBlockAndWait:^{
      blockCopy(context);
      [context save:NULL];  //Don't just pass NULL here, look at the error and log it to your analytics service
     }];
  }]];
}

 //swift
func enqueue(block: @escaping (_ context: NSManagedObjectContext) -> Void) {
  persistentContainerQueue.addOperation(){
    let context: NSManagedObjectContext = self.persistentContainer.newBackgroundContext()
      context.performAndWait{
        block(context)
        try? context.save() //Don't just use '?' here look at the error and log it to your analytics service
      }
    }
}

When you call enqueueCoreDataBlock the block is enqueued to ensures that there are no merge conflicts. But if you write to the viewContext that would defeat this setup. Likewise you should treat any other contexts that you create (with newBackgroundContext or with performBackgroundTask) as readonly because they will also be outside of the writing queue.

At first I thought that NSPersistentContainer's performBackgroundTask had an internal queue, and initial testing supported that. After more testing I saw that it could also lead to merge conflicts.

like image 149
Jon Rose Avatar answered Sep 25 '22 02:09

Jon Rose