Note that there are some explanatory texts on larger screens.

plurals
  1. POHow to make changes to Core Data objects from different threads without having to save after every change
    primarykey
    data
    text
    <p>I have perused all related threads on SO but still confused about how to make changes to core data objects from multiple threads without having to save after every change.</p> <p>I am working on an app that talks to the server constantly. The app uses Core Data for storage and <code>NSFetchedResultsController</code> is used in a few view controllers to fetch data from the persistence store. Usually when the user performs an action, a network request will be triggered. Before the network request is sent, usually some changes should be made to relevant Core Data objects, and upon server response, more changes would be made to those Core Data objects.</p> <p>Initially all the Core Data operations were done on the main thread in the same <code>NSManagedObjectContext</code>. It was all well except that when network traffic is high, the app can become unresponsive for several seconds. Obviously that's not acceptable so I looked into moving some Core Data operations to run in the background.</p> <p>The first approach I tried was to create an NSOperation object to process each network response. Inside the main method of the NSOperation object, I set up a dedicated MOC, make some changes, and commit the changes at the end.</p> <pre><code>- (void)main { @try { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; // Create a dedicated MOC for this NSOperation NSManagedObjectContext * context = [[NSManagedObjectContext alloc] init]; [context setPersistentStoreCoordinator:[APP_DELEGATE persistentStoreCoordinator]]; // Make change to Core Data objects // ... // Commit the changes NSError *error = nil; if ([context hasChanges] &amp;&amp; ![context save:&amp;error]) { NSLog(@"Unresolved error %@, %@", error, [error userInfo]); } // Release the MOC [context release]; // Drain the pool [pool drain]; } @catch (NSException *exception) { // Important that we don't rethrow exception here NSLog(@"Exception: %@", exception); } } </code></pre> <p>The MOC on the main thread is registered for the <code>NSManagedObjectContextDidSaveNotification</code>.</p> <pre><code>[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:nil]; </code></pre> <p>So when the background context commits changes, the main MOC will be notified and then will merge in the changes:</p> <pre><code>- (void)backgroundContextDidSave:(NSNotification *)notification { // Make sure we're on the main thread when updating the main context if (![NSThread isMainThread]) { [self performSelectorOnMainThread:@selector(backgroundContextDidSave:) withObject:notification waitUntilDone:NO]; return; } // Merge the changes into the main context [[self managedObjectContext] mergeChangesFromContextDidSaveNotification:notification]; } </code></pre> <p>However, as I mentioned earlier, I also need to make changes to Core Data objects from the main MOC. Each change is usually very small (e.g. update one instance variable in an object) but there can be many of them. So I really don't want to save the main MOC after every single change. But if I don't do that, I run into problems when merging changes from background MOC to the main MOC. Merge conflicts occur since both MOCs have unsaved changes. Setting the merge policy doesn't help either, since I want to keep changes from both MOCs.</p> <p>One possibility is to register the background MOC with the <code>NSManagedObjectContextDidSaveNotification</code> as well, but that approach smells like bad design for me. And I would still need to save the main MOC after every single change.</p> <p>The second approach I tried was to do all Core Data changes from a dedicated background context running on a permanent background thread.</p> <pre><code>- (NSThread *)backgroundThread { if (backgroundThread_ == nil) { backgroundThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(backgroundThreadMain) object:nil]; // Actually start the thread [backgroundThread_ start]; } return backgroundThread_; } // Entry point of the background thread - (void)backgroundThreadMain { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; // We can't run the runloop unless it has an associated input source or a timer, so we'll just create a timer that will never fire. [NSTimer scheduledTimerWithTimeInterval:DBL_MAX target:self selector:@selector(ignore) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] run]; // Create a dedicated NSManagedObjectContext for this thread. backgroundContext_ = [[NSManagedObjectContext alloc] init]; [backgroundContext_ setPersistentStoreCoordinator:[self persistentStoreCoordinator]]; [pool drain]; } </code></pre> <p>So whenever I need to make Core Data changes from the main thread, I have to get the objectID from the main thread, and pass to the background thread to perform the change. When the background context saves, the changes will then be merged back to the main MOC.</p> <pre><code>- (void)addProduct:(Product *)product toCatalog:(Catalog *)catalog; </code></pre> <p>would change to:</p> <pre><code>- (void)addProduct:(NSManagedObjectID *)productObjectId toCatalog:(NSManagedObjectID *)catalogObjectId { NSArray * params = [NSArray dictionaryWithObjects:productObjectId, catalogObjectId, nil]; [self performSelector:(addProductToCatalogInBackground:) onThread:backgroundThread_ withObject:params waitUntilDone:NO]; } </code></pre> <p>But this seems so convoluted and ugly. Writing code like this seems to negate the usefulness of using Core Data in the first place. Also, I would still have to save the MOC after every single change since I can't get objectId for a new object without saving it to the datastore first.</p> <p>I feel that I am missing something here. I really hope someone can shed some light on this. Thanks.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload