10

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.

dav_i
  • 27,509
  • 17
  • 104
  • 136
Kevin Renskers
  • 5,156
  • 4
  • 47
  • 95
  • The new nested contexts have started to wreak havoc on much of the saving APIs in MagicalRecord. While I'm aware of these issues, and some fixes are being discussed now, I'm always open to suggestions. – casademora Sep 24 '12 at 17:41
  • 2
    Perhaps you're best off using Core Data without a framework like MR? – Hunter Nov 11 '12 at 19:45
  • Did you ever find a solution using MagicalRecord? I have having the same issues (UI locking while updating in the background) and I cannot find a solution. Thanks! – RyanG Jan 07 '13 at 21:24
  • In the end I removed MagicalRecord from my app. I now use https://github.com/jksk/NLCoreData, which is a lot less magic, and seems to work much better. – Kevin Renskers Jan 08 '13 at 09:19
  • Is MagicalRecord 2.1 behaving better? Does background saving actually work now? – Kevin Renskers Jan 10 '13 at 14:58
  • If this is in any way similar to using pure core data (without MR) than what you observe while saving in back thread is normal behaviour. Since that is a child context it saves to memory, parent contexts task is to save it to the disk. So I guess you have your data in memory, to test that try to fetch after save. If you want to avoid blocking the main thread, than save your parent context on "applicationDidEnterBackground"... – AntonijoDev Dec 20 '13 at 14:44
  • Saving when the app goes to background is a bit too risky. If the app crashes before then, everything is lost. – Kevin Renskers Jan 07 '14 at 09:49

2 Answers2

6

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];
    });
  }
}
Community
  • 1
  • 1
Onato
  • 9,916
  • 5
  • 46
  • 54
  • how does this exactly work? I'm facing the problem that my main thread is blocking because of large amounts of data to import. I'm using MagicalRecord but don't manage to achieve what you're writing here... – swalkner Dec 19 '13 at 15:31
1

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

Community
  • 1
  • 1
Imran Ali Khan
  • 8,469
  • 16
  • 52
  • 77