Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create multiple objects in background?

I'm using MagicalRecord 2.0.3 and I can't really figure out how to save data in the background.

According to the documentation, something like this should work:

[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext) {
    // Do this hundreds of times
    [MyObject createInContext:localContext];
}];

However, nothing is saved to the database. I've seen multiple people posting solutions similar to this:

[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext) {
    // Do this hundreds of times
    [MyObject createInContext:localContext];
} completion:^{
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [[NSManagedObjectContext defaultContext] saveNestedContexts];
    }];
}];

This does save my data in the database, however since the save happens on the main thread, my application is unresponsive for a while (with my dataset, about 3 seconds which is way too long).

I've also tried this, but it also blocks up while saving:

self.queue = [[NSOperationQueue alloc] init];

[self.queue addOperationWithBlock:^{
    NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];

    // Do this hundreds of times
    [MyObject createInContext:localContext];

    [localContext saveNestedContexts];
}];

And lastly, same blocking effect with this code:

dispatch_queue_t syncQueue = dispatch_queue_create("Sync queue", NULL);
dispatch_async(syncQueue, ^{
    NSManagedObjectContext *localContext = [NSManagedObjectContext contextForCurrentThread];

    // Do this hundreds of times
    [MyObject createInContext:localContext];

    [[NSManagedObjectContext contextForCurrentThread] saveNestedContexts];
});

So, what is the best way to solve this? I need to create hundreds of objects in the background and the app needs to remain responsive.

like image 421
Kevin Renskers Avatar asked Sep 24 '12 16:09

Kevin Renskers


2 Answers

MagicalRecord uses a child context when doing work in the background. This works fine for small changes, but will create excessive main thread blocking when importing large amounts of data.

The way to do it is to use a parallel NSManagedObjectContext and to do the merging yourself with the NSManagedObjectContextDidSaveNotification notification and the mergeChangesFromContextDidSaveNotification method. See performance tests here: http://floriankugler.com/blog/2013/5/11/backstage-with-nested-managed-object-contexts

When saving a nested contexts everything has to be copied to the parent context. As opposed to this, objects that have not been fetched (in the context into which you are merging) will not be merged by mergeChangesFromContextDidSaveNotification. This is what makes it faster.

You might encounter problems if you want to display these results right away after saving in batches and using an NSFetchResultsController. See the following question for a solution: NSFetchedResultsController with predicate ignores changes merged from different NSManagedObjectContext

For more performance tips take a look at this question: Implementing Fast and Efficient Core Data Import on iOS 5

Create your own context.

NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] 
                          initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[importContext setPersistentStoreCoordinator:yourPersistentStoreCoordinator];
[importContext setUndoManager:nil]; // For importing you don't need undo: Faster

// do your importing with the new importContext
// …

NSError* error = nil;
if(importContext.hasChanges) {
  if(![importContext save:&error]) {
      NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
  } 
}

Make sure you are listening for the saves to managed object contexts.

[[NSNotificationCenter defaultCenter] 
              addObserver:singleton 
                 selector:@selector(contextDidSave:)
                     name:NSManagedObjectContextDidSaveNotification object:nil];

In the contextDidSave:you merge the change yourself.

- (void) contextDidSave:(NSNotification*) notification
{
  if(![notification.object isEqual:self.mainContext]) {
    dispatch_async(dispatch_get_main_queue(), ^{
      [self.mainContext mergeChangesFromContextDidSaveNotification:notification];
    });
  }
}
like image 170
Onato Avatar answered Nov 06 '22 11:11

Onato


Managed object contexts are not thread safe so if you ever need to do any kind of background work with your Coredata objects (i.e. a long running import/export function without blocking the main UI) you will want to do that on a background thread.

In these cases you will need to create a new managed object context on the background thread, iterate through your coredata operation and then notify the main context of your changes.

You can find an example of how this could work here

Core Data and threads / Grand Central Dispatch

like image 21
Imran Ali Khan Avatar answered Nov 06 '22 11:11

Imran Ali Khan