📜 ⬆️ ⬇️

Multicontext in core data

Hello.

When you start using CoreData to store data in your applications, you start working with a single managed objective context / managed object context (MOC). This is what is used in the template when creating a project in xCode, if you tick the box next to “Use Core Data” when creating the project.

image
')


Using CoreData in combination with NSFetchedResultsController greatly simplifies working with any kind of list of items that are displayed on the screen in a table view.

There are two scenarios in which you would like to branch out, i.e. using several managed objective contexts: 1) to simplify the process of adding / editing new elements and 2) to avoid blocking the UI. In this post, I want to explore ways to create your contexts in order to get what you want.

First, let's look at the installation of a single context. You need a persistent store coordinator (PSC) to access the database file on disk. So that this coordinator understands how the database is structured, you need a model. This model is combined from all the model definitions contained in the project, and indicates CoreData about this database structure. The coordinator is installed on the managed context object through the function property. Remember the first rule: a managed objective context with the help of the coordinator is written to the disk if you call saveContext.

image

Consider this scheme. Each time you insert, update or delete an entity in this single controlled objective context, the controller of the selected results will be notified of these changes and will update its content in the table view. It does not depend on the preservation of the context. You can save as rarely or as often as you want. The Apple template saves on every addition of an entity and also (if not strange) on applicationWillTerminate.

This approach is mainly suitable for most major cases, but as I said before, there are two problems with it. The first is related to the addition of a new entity. You probably want to use the same visual representation again, to add / edit an entity. So you might want to create a new entity even before you fill out the view visualization for it. This would cause update notifications to trigger an update on the controller of the selected results, i.e. a blank line will appear shortly before the MVC concept has fully appeared for adding or editing.

The second problem would be obvious if the updates accumulated before the saveContext became too extensive, and the save operation would take longer than 1 / 60th of a second. Because in this case the user interface would be blocked until the save was completed and you would have a meaningful transition, for example, when scrolling.

Both problems can be solved using several manageable objective contexts.

"Traditional" multi-context approach

Think of each managed objective context as a temporary notepad of changes. Before the release of iOS 5, you probably heard about changes in other contexts and merged the changes from the moment you were notified to the main context. A typical installation would look like this flowchart:

image

Create a temporary context to use for the background task queue. And save the changes there, set the same resident storage coordinator in the temporary context as in the main context. According to Marcus Sarr, it should look like this:

Although the NSPersistentStoreCoordinator is not thread safe, NSManagedObjectContext knows how to block it properly when it is in use. Therefore, we can attach as many NSManagedObjectContext objects to NSPersistentStoreCoordinator as we want without fear of a collision.

Calling saveContext in the background context will write the changes to the storage file and also initiate NSManagedObjectContextDidSaveNotification.

In the code, it will look something like this:

dispatch_async(_backgroundQueue, ^{ // create context for background NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] init]; tmpContext.persistentStoreCoordinator = _persistentStoreCoordinator; // something that takes long NSError *error; if (![tmpContext save:&error]) { // handle error } }); 


Creating a temporary context happens very quickly, so you don't have to worry about the frequent creation and release of these temporary contexts. The fact is that in order to set persistentStoreCoordinator to the same main context so, creation must also occur in the background.

I prefer this simplified installation of the CoreData stack:

 - (void)_setupCoreDataStack { // setup managed object model NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Database" withExtension:@"momd"]; _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; // setup persistent store coordinator NSURL *storeURL = [NSURL fileURLWithPath:[[NSString cachesPath] stringByAppendingPathComponent:@"Database.db"]]; NSError *error = nil; _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel]; if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error]) { // handle error } // create MOC _managedObjectContext = [[NSManagedObjectContext alloc] init]; [_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator]; // subscribe to change notifications [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_mocDidSaveNotification:) name:NSManagedObjectContextDidSaveNotification object:nil]; } 


Now consider the notification handler that we set so that the didSave notification pops up every time.

 - (void)_mocDidSaveNotification:(NSNotification *)notification { NSManagedObjectContext *savedContext = [notification object]; // ignore change notifications for the main MOC if (_managedObjectContext == savedContext) { return; } if (_managedObjectContext.persistentStoreCoordinator != savedContext.persistentStoreCoordinator) { // that's another database return; } dispatch_sync(dispatch_get_main_queue(), ^{ [_managedObjectContext mergeChangesFromContextDidSaveNotification:notification]; }); } 


First, we want to avoid merging our own changes. Also, if we have several CoreData databases in the same application, we are trying to avoid merging changes that are intended for another database. I encountered such a problem in one of my applications, which is why I check the persistent storage coordinator. Finally, merge the changes using the mergeChangesFromContextDidSaveNotification method. A notification has a dictionary of all changes in its payload, and this method is aware of their integration into the context.

Transferring managed objects between contexts

It is strictly forbidden to move a managed object that you received from one context to another. There is an easy way to deal with the “mirror” of a managed object via ObjectID. This identifier is multi-threaded execution oriented, and you can always get it from a single NSManagedObject instance and then call objectWithID. The second context will then receive its own copy of the managed objects for working with it.

 NSManagedObjectID *userID = user.objectID; // make a temporary MOC dispatch_async(_backgroundQueue, ^{ // create context for background NSManagedObjectContext *tmpContext = [[NSManagedObjectContext alloc] init]; tmpContext.persistentStoreCoordinator = _persistentStoreCoordinator; // user for background TwitterUser *localUser = [tmpContext objectWithID:userID]; // background work }); 


The described approach is fully backward compatible up to the first version of IOS, which received support for CoreData from IOS 3. If you need only IOS 5 support for your application, then there is a more modern approach, which we will consider below.

Parent / child context

IOS 5 introduced the ability for a managed object context to contain parentContext. Calling the saveContext method pushes the changes from the child context to the parent without having to resort to a method that includes merging content from the dictionary that describes the changes. At the same time, Apple added the ability for contexts to have their own separate queue for making changes both synchronously and asynchronously.

The type of queue concurrency is specified in the new initializer initWithConcurrencyType on NSManagedObjectContext. Notice that in this scheme I have added several child contexts, so everyone has the same main context queue as the parent.

image

The child context each time, when saved, will save the changes to its parent, and this leads to the fact that the controller of the selected results must also be aware of these changes. However, this does not yet save data, since the background context does not know about the persistent storage coordinator. To get data to disk, you need an additional method saveContext: on the main context queue.

The first change required for this approach is to change the main concurrency type context to NSMainQueueConcurrencyType. In the aforementioned _setupCoreDataStack, the changes to the initial line are as shown below and there is no longer a need to receive merge notifications.

 _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; [_managedObjectContext setPersistentStoreCoordinator:_persistentStoreCoordinator]; 


A lengthy background operation will look like this:

 NSMangedObjectContext *temporaryContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; temporaryContext.parentContext = mainMOC; [temporaryContext performBlock:^{ // do something that takes some time asynchronously using the temp context // push to parent NSError *error; if (![temporaryContext save:&error]) { // handle error } // save parent to disk asynchronously [mainMOC performBlock:^{ NSError *error; if (![mainMOC save:&error]) { // handle error } }]; }]; 


Each context now needs to use performBlock: (async) or performBlockAndWait: (sync) to work. This ensures that the operations contained in the block use the correct queue. In the above example, a long operation is performed on a background queue. Once everything is ready for you, and the changes are redirected to the parent via the saveContext method, then an asynchronous performBlock method will appear to save the mainMOC. And will happen again on the correct queue, as provided by performBlock.

Child contexts, unlike parents, are not automatically updated. You can download them again to get updates, but in most cases they are temporary, and so we don’t have to worry about it. As long as the main context queue receives changes, the controllers of the selected results are updated, and we have persistence while maintaining the main context.

The amazing simplification provided by this approach is that you can create a temporary context (as a child) for any visualization of the view that has the Save and Cancel buttons. If you pass a managed object for editing, then you transfer it (via the objectID mentioned above) to a temporary context. The user has the ability to update all elements of the managed object. If he clicks on Save, then the entire temporary context is saved. If he clicks on cancel, then nothing needs to be done, because the changes are discarded along with the temporary context.

Don't you have a headache with all this information? If not, here is the aerobatics about CoreData multi-context.

Asynchronous data storage

Core Data guru Marcus Zarra showed me the following approach, which is based on the above-mentioned Parent / Child method, but adds additional context solely for writing to disk. As mentioned earlier, a long write operation could block the main thread for a short time, freezing the UI. Within this reasonable approach, the recording is allocated in a separate queue, and the user interface keeps the operation smooth (it remains smooth, it does not “hang”).

image

Setting up for CoreData is also quite simple. You just need to move the persistentStoreCoordinator to our new hidden context and make the main context a child element.

 // create writer MOC _privateWriterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; [_privateWriterContext setPersistentStoreCoordinator:_persistentStoreCoordinator]; // create main thread MOC _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; _managedObjectContext.parentContext = _privateWriterContext; 


Now we need to make three different saves for each update: temporary context, main UI context and for recording to disk. But just as easily as before, it is possible to implement the performBlocks stack ... The user interface remains unlocked during a long database operation (for example, importing a large number of records), as well as when it is written to disk.

Conclusion

iOS 5 significantly simplified working with CoreData on background queues, and received changes emanating from child contexts to their parents. If you are still using iOS 3/4 then all these features are not available to you. But if you are starting a new project that has iOS 5 as the minimum requirement, you can immediately create a Marcus Sarra Turbo Approach as described above.

Zack Woldowski pointed out to me that using hidden type of queue parallelism to “edit the rendering of a view” may be redundant. If you use NSContainmentConcurrencyType instead of rendering the representation of the child context then you do not need the performBlock wrapper. All you need is performBlock on mainMOC to save.

The type of restriction concurrency is the “old way” of executing contexts, but this does not mean that it was traditional. It simply binds the operations of the context to the self-managed threading model. The set of turns of the hidden queue for each new controller is wasteful, unnecessary, and slow.

NSManagedObjectContext knows how to save and merge reasonably, and therefore the main thread context is bound to the main thread, its merges are always performed safely. Editing a view visualization is associated with the main thread in the same way as the main presentation visualization; the only way is - a separate operation, which is only in the UI, so it is suitable for using the constraint parallelism type here. The editing context is not conceptually a “new” thing, it simply postpones the change to later, while still allowing you to undo the changes completely.

Thus, it really comes down to your personal preference: a hidden queue with performBlock or no restriction parallelism. As for me, I try to prefer hidden queues due to the security that I get from using them.

ps For many it may seem that the article is useless, but I hope that some of them will still endure something useful from this article. Do not scold much for the translation, if there are comments, write in a personal, correct :)

Source: https://habr.com/ru/post/238901/


All Articles