20

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

Cœur
  • 37,241
  • 25
  • 195
  • 267
MFA
  • 537
  • 2
  • 6
  • 16

1 Answers1

29

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 will 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.

Jon Rose
  • 8,373
  • 1
  • 30
  • 36
  • thanks for the explanation. if I create a NSManagedObject, I should do it in `performBackgroundTask` and then call save on the private context given in the closure? – MFA Mar 12 '17 at 08:59
  • 2
    All creation and updating of managedObjects should be done in a `performBackgroundTask` block using the context that is given to that block. Don't forget to call save at then end. Also make sure you have ` self.persistentContainer.viewContext.automaticallyMergesChangesFromParent = true;` in your core-data setup code. – Jon Rose Mar 12 '17 at 09:03
  • Ok, I did that and it's working but it's still not what I expected from concurrency, UI still get blocked when I call save at the end of `performBackgroundTask` block but I think this is a new question. I'll be grateful if you happen to know of a link to answer this, also covering how `NSFetchedResultController` should be used in par with `performBackgroundTask`. – MFA Mar 12 '17 at 09:20
  • The UI should not be blocking for a save. Either you are doing something wrong in the save (calling `perform` or `performAndWait`?) of you have a fetchedResultsController doing too much stuff on a change (a reload in didChangeObject ?) – Jon Rose Mar 12 '17 at 09:24
  • `NSFetchedResultController` should not really be effected by `performBackgroundTask`. The fetchResultsController monitor for changes and update the UI; `performBackgroundTask` makes the changes. Neither one needs to know how the other is working. – Jon Rose Mar 12 '17 at 09:25
  • Got it, thanks a lot, this problem was caused by sth else. One last thing, if I have two `performBackgroundTask` blocks and call save on both `moc`s almost at same time, the merge will fail or core data knows how to handle this? should I be waiting for other block to finish save before I start a new `performBackgroundTask`? I really appreciate your help – MFA Mar 12 '17 at 12:57
  • They will not happen at the same time. `performBackgroundTask` internally has a serial queue - only one will run at a time. A background task won't start until all the ones before it have finished. – Jon Rose Mar 12 '17 at 13:21
  • 2
    I don't think that the "read only" note on that page means that the view context should be used only for reading data. Rather it's saying that the property itself is read only, that you can't create another context and assign it as the view context. – Tom Harrington Jul 14 '17 at 04:39
  • Rereading the documentation I think that you have a point. Still if you write the view context and write using performBackgroundTask you are going to get write conflicts that will cause you to loose data. I will update my answer accordingly. – Jon Rose Jul 16 '17 at 05:17
  • What about using `newBackgroundContext()` instead of `performBackgroundTask`? This way, you can create new entities, present them in a table, and only save to `viewContext` when the user taps a save button. As far as I can see this cannot be done with `performBackgroundTask`, is that correct? – koen Jul 18 '17 at 11:31
  • 1
    If you are going to be displaying the items in a tableView you need a main queue context and `newBackgroundContext` will give you a background context. Also saving a context created with `newBackgroundContext` can also lead to write conflicts. – Jon Rose Jul 18 '17 at 11:37
  • So a/the way to go would be to just use `viewContext` and only save it when the user hits save and discard all changes when the user hits cancel? – koen Jul 18 '17 at 12:18
  • 1
    I think Apple designed parent-child context exactly for this problem. I have never personally dealt with this particular problem and have never used parent-child context. For a small amount of data, I would display the data back by in-memory variables, and only save it to core data (using performBackgroundTask) when the user presses save. Any writing that doesn't use `performBackgroundTask` could lead to merge conflicts. – Jon Rose Jul 18 '17 at 13:08
  • Isn't the context from `newBackgroundContext()` or `performBackgroundTask` a child of `viewContext`? – koen Jul 18 '17 at 15:04
  • I'm still confused, so I posted a new question here: https://stackoverflow.com/questions/45176830/core-data-concurrency-with-nspersistentcontainer – koen Jul 18 '17 at 21:00
  • 1
    @Koen: a background context is a Managed Object Context operating on a private queue, that is it's not meant to do operations which are gonna be used by the views which are operating only on the main queue. You are making confusion between a parent-child context pattern and the queue ("threads") where the context are operating. – valeCocoa Jul 30 '17 at 19:09
  • 1
    @Jon Rose: I haven't yet got the chance to work a bit with performBackgroundTask: and I'd like to understand something: shall we use the usual performBlock and performBlockAndWait selectors inside this block for the given MOC, or can we just freely call the MOC selectors as if this block were executed on the same queue of the MOC? – valeCocoa Jul 30 '17 at 19:13
  • performBackgroundTask is executed on the thread appropriate for the the context that is passed. The problem with using only performBackgroundTask is that there is no queue, so multiple writes can happen at the same time and lead to conflicts. – Jon Rose Jul 31 '17 at 05:24
  • 7
    @JonRose It seems Apple have no idea of how to make CoreData stack less painful and confusing lol – m8labs Mar 31 '18 at 12:26