Nspersistentcontainer Concurrency for Saving to Core Data

NSPersistentContainer concurrency for saving to core data

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.

Core Data concurrency with NSPersistentContainer

If you have one page of forms and you want it to save when the user presses the save button, then simply take the data from the textFields (or whatever your data input is) and put it into core data using performBackgroundTask. Since the data is only stored in the textFields while the user is editing if the user pushes back his edits will be lost.

If you have a lots of changes to a complex document with lots of different entities that the user can create or destroy or link and all of that is only saved when the user presses save then you should use a child context. You would display the data based on the values in the child context but only push those changes to the parent context if the user presses save. This is a very rare situation and I have never personally encountered the need to do this.

I strongly suspect that you are in the first case. Don't use child context. Use performBackgroundTask and save the data when the user presses save.

(also the correct context to use inside the block [private performBlock:^{ is the private context)

How to change the concurrency type for Core Data in Swift

That code creates an NSPersistentContainer, then gets the viewContext from the container. The container can also create other contexts, if you want. You can either call container.newBackgroundContext() to create a new context like the one you describe (it will use private queue concurrency). You can also use context.performBackgroundTask(_:) to have the container create a new private-queue context on the fly for some background work.

Merge conflict when multiple core data inserts are dispatched to NSPersistentContainer using performBackgroundTask

I wrote the answer that you are quoting. I was wrong. I have updated it.

I have found that NSPersistentContainer's performBackgroundTask does not have a functional internal queue and it can lead to merge conflicts. When I initially tested it, it seemed like it did, but I found out like you that there can be conflicts. Luckily it is not that hard to fix by creating your own queue. I know it seems strange for Apple to release something that is so broken, but that seems to be the case.

I am sorry for posted incorrect information.

How to use Core Data managedObjectContext properly and a proper way to save context?

You have two options:

The simple way

Read and write ONLY to the viewContext and ONLY from the main thread. This will work but if you have a lot of data that you are reading or writing at one time it will block your UI. If you have an app with a small amount of stuff stored in core-data this will work fine and is by far the simplest option.

The complex way

Do all of your reading with the viewContext but NEVER write to it. Only access the viewContext from the main thread. Do all of your writing with persistentContainer.performBackgroundTask and use the context that was pass into the block. Do not use any other managedObjects inside this block. If you need to then pass an objectId and do a fetch inside the block. Don't pass any objects outside of these blocks. If you find that you need to, then use objectId. You also need to create an operationQueue and wrap all you calls to performBackgroundTask inside of it so you don't get any write conflicts. A fuller description can be found here: NSPersistentContainer concurrency for saving to core data

NSPersistentContainer / Core Data / ReadOnly store

Set the option in the description

description.setOption(NSNumber(value: true), forKey: NSReadOnlyPersistentStoreOption)

By the way: The variable persistentStoreDescriptions is unused.

Tear down and rebuild Core Data Stack with NSPersistentContainer

Anything you have been doing before should still work the same way. A NSPersistentContainer is not magic, and does not do that much. It is a convenience for setting up a store with a model and a main thread context. It is has methods for creating background context. It does not replace everything you know about core-data. It is all the same stuff inside. Inside the persistentContainer is a pointer to the persistentStoreCoordinator which has a pointer to the model and to the stores.

I don't know what you code is for replacing a store, so I can't help you translate it to a persistentContainer. If you find you can't do, there is no reason to stick to NSPersistentContainer, as I said before - it is not magic and it doesn't do that much for you.



Related Topics



Leave a reply



Submit