核心数据背景上下文最佳实践

时间:2014-07-09 15:13:36

标签: ios objective-c core-data nsmanagedobjectcontext

我需要对核心数据进行大量的导入任务 假设我的核心数据模型如下所示:

Car
----
identifier 
type

我从我的服务器获取汽车信息JSON列表,然后我想将它与我的核心数据Car对象同步,这意味着:
如果它是一辆新车 - >从新信息中创建新的Core Data Car对象 如果汽车已经存在 - >更新核心数据Car对象。

所以我想在后台执行此导入而不阻止UI,而使用滚动显示所有汽车的汽车表视图。

目前我正在做这样的事情:

// create background context
NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

[bgContext performBlock:^{
    NSArray *newCarsInfo = [self fetchNewCarInfoFromServer]; 

    // import the new data to Core Data...
    // I'm trying to do an efficient import here,
    // with few fetches as I can, and in batches
    for (... num of batches ...) {

        // do batch import...

        // save bg context in the end of each batch
        [bgContext save:&error];
    }

    // when all import batches are over I call save on the main context

    // save
    NSError *error = nil;
    [self.mainContext save:&error];
}];

但我不确定我在这里做的是正确的事,例如:

我可以使用setParentContext吗? 我看到一些像这样使用它的例子,但是我看到了其他没有调用setParentContext的例子,而是他们做了类似的事情:

NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;  
bgContext.undoManager = nil;

我不确定的另一件事是什么时候在主要上下文中调用save,在我的例子中我只是在导入结束时调用save,但是我看到了使用的示例:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {
    NSManagedObjectContext *moc = self.managedObjectContext;
    if (note.object != moc) {
        [moc performBlock:^(){
            [moc mergeChangesFromContextDidSaveNotification:note];
        }];
    }
}];  

正如我之前提到的,我希望用户能够在更新时与数据进行交互,那么如果用户在导入更改同一辆车时更改了车型,那么我写的方式是安全的吗? / p>

更新

感谢@TheBasicMind很好的解释我正在尝试实现选项A,所以我的代码看起来像:

这是AppDelegate中的核心数据配置:

AppDelegate.m  

#pragma mark - Core Data stack

- (void)saveContext {
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
            DDLogError(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}  

// main
- (NSManagedObjectContext *)managedObjectContext {
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    _managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    _managedObjectContext.parentContext = [self saveManagedObjectContext];

    return _managedObjectContext;
}

// save context, parent of main context
- (NSManagedObjectContext *)saveManagedObjectContext {
    if (_writerManagedObjectContext != nil) {
        return _writerManagedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _writerManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
        [_writerManagedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _writerManagedObjectContext;
}  

这就是我的导入方法现在的样子:

- (void)import {
    NSManagedObjectContext *saveObjectContext = [AppDelegate saveManagedObjectContext];

    // create background context
    NSManagedObjectContext *bgContext = [[NSManagedObjectContext alloc]initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    bgContext.parentContext = saveObjectContext;

    [bgContext performBlock:^{
        NSArray *newCarsInfo = [self fetchNewCarInfoFromServer];

        // import the new data to Core Data...
        // I'm trying to do an efficient import here,
        // with few fetches as I can, and in batches
        for (... num of batches ...) {

            // do batch import...

            // save bg context in the end of each batch
            [bgContext save:&error];
        }

        // no call here for main save...
        // instead use NSManagedObjectContextDidSaveNotification to merge changes
    }];
}  

我还有以下观察员:

[[NSNotificationCenter defaultCenter] addObserverForName:NSManagedObjectContextDidSaveNotification object:nil queue:nil usingBlock:^(NSNotification* note) {

    NSManagedObjectContext *mainContext = self.managedObjectContext;
    NSManagedObjectContext *otherMoc = note.object;

    if (otherMoc.persistentStoreCoordinator == mainContext.persistentStoreCoordinator) {
        if (otherMoc != mainContext) {
            [mainContext performBlock:^(){
                [mainContext mergeChangesFromContextDidSaveNotification:note];
            }];
        }
    }
}];

3 个答案:

答案 0 :(得分:167)

对于第一次接触Core Data的人来说,这是一个非常令人困惑的话题。我不能轻易说出来,但有经验的话,我有信心说苹果文档在这个问题上有些误导(如果你仔细阅读,它实际上是一致的,但它们并没有充分说明为什么合并数据在许多情况下仍然是比依赖父/子上下文并简单地从子节点保存到父节点更好的解决方案。

文档给出了强烈的印象,父/子上下文是进行后台处理的新首选方式。然而,苹果忽视了一些强烈的警告。首先,请注意,您在孩子上下文中获取的所有内容都是首先通过它的父级。因此,最好将主线程上运行的主上下文的任何子进程限制为处理(编辑)已在主线程上的UI中呈现的数据。如果您将它用于一般同步任务,您可能希望处理的数据远远超出您当前在UI中显示的范围。即使您使用NSPrivateQueueConcurrencyType,对于子编辑上下文,您可能会在主上下文中拖动大量数据,这可能会导致性能下降和阻塞。现在最好不要将主上下文作为用于同步的上下文的子项,因为它不会被通知同步更新,除非您要手动执行此操作,此外您将执行可能长时间运行的任务在上下文中,您可能需要响应从作为主要上下文的子项的编辑上下文通过级联启动的保存,通过主联系人向下发送到数据存储。您必须手动合并数据,还可能跟踪主要上下文中需要失效的内容并重新同步。不是最简单的模式。

Apple文档没有说清楚的是,您最有可能需要混合使用描述" old"处理线程约束的方式,以及新的Parent-Child上下文服务方式。

最好的选择可能是(我在这里提供通用解决方案,最佳解决方案可能取决于您的详细要求),将NSPrivateQueueConcurrencyType保存上下文作为最顶层的父级,直接保存到数据存储区。 [编辑:你不会直接在这个上下文中做很多事情],然后给那个保存上下文至少两个直接的孩子。一个你用于UI的NSMainQueueConcurrencyType主上下文[编辑:它最好遵纪守法,避免对此上下文进行任何编辑],另一个是NSPrivateQueueConcurrencyType,用于对数据进行用户编辑以及(在附图中的选项A中)您的同步任务。

然后,将主上下文作为同步上下文生成的NSManagedObjectContextDidSave通知的目标,并将通知.userInfo字典发送到主上下文的mergeChangesFromContextDidSaveNotification:。

要考虑的下一个问题是放置用户编辑上下文的位置(用户编辑的上下文会反映回界面)。如果用户的操作始终局限于对少量呈现数据的编辑,那么使用NSPrivateQueueConcurrencyType再次将其作为主要上下文的子项是您最好的选择并且最容易管理(保存然后将编辑直接保存到主上下文,如果你有一个NSFetchedResultsController,将自动调用相应的委托方法,以便你的UI可以处理更新控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath :)(再次这是选项A)。

另一方面,如果用户操作可能导致处理大量数据,您可能需要考虑将其作为主上下文和同步上下文的另一个对等方,以使保存上下文具有三个直接子级。 主要同步(专用队列类型)和编辑(专用队列类型)。我已将此安排显示为图表中的选项B.

与同步上下文类似,您需要[编辑:配置主要上下文以接收通知]保存数据(或者如果您需要更多粒度,更新数据时)并采取措施合并数据(通常使用mergeChangesFromContextDidSaveNotification :)。请注意,通过这种安排,主要上下文不需要调用save:方法。 enter image description here

要理解父/子关系,请选择选项A:父子方法只是意味着如果编辑上下文获取NSManagedObjects,它们将被复制到" (注册)首先保存上下文,然后是主上下文,然后最后编辑上下文。您可以对它们进行更改,然后在调用save时:在编辑上下文中,更改将保存到主上下文。您必须在主上下文中调用save:然后在保存上下文中调用save:才能将它们写入磁盘。

从子级保存到父级时,会触发各种NSManagedObject更改和保存通知。因此,例如,如果您使用获取结果控制器来管理UI的数据,那么将调用它的委托方法,以便您可以根据需要更新UI。

一些结果:如果在编辑上下文中获取对象和NSManagedObject A,则修改它并保存,以便将修改返回到主上下文。现在,您已在主要和编辑上下文中注册了已修改的对象。这样做会很糟糕,但是你现在可以在主上下文中再次修改对象,它现在将与对象不同,因为它存储在编辑上下文中。如果您尝试对存储在编辑上下文中的对象进行进一步修改,则您的修改将与主上下文中的对象不同步,并且任何保存编辑上下文的尝试都将引发错误。

出于这个原因,使用类似选项A的安排,尝试获取对象,修改它们,保存它们并重置编辑上下文(例如,[editContext reset]是运行循环的任何一次迭代)是一个很好的模式(或者在传递给[editContext performBlock:]的任何给定块内)。最好是遵守纪律,避免在主上下文中进行任何编辑。 此外,要重新迭代,因为main上的所有处理都是主线程,如果您将大量对象提取到编辑上下文,主上下文将在主线程上执行它的获取处理 正在将这些对象从父上下文迭代地复制到子上下文中。如果正在处理大量数据,则可能导致UI中的无响应。因此,例如,如果您拥有大量托管对象,并且您有一个UI选项,则会导致所有这些对象都被编辑。在这种情况下,配置您的应用程序就像选项A一样是个坏主意。在这种情况下,选项B是更好的选择。

如果您没有处理数千个对象,那么选项A可能就足够了。

BTW不必过于担心您选择哪个选项。从A开始并且如果您需要更改为B可能是一个好主意。它比您想象的更容易做出这样的改变,并且通常比您预期的后果更少。

答案 1 :(得分:13)

首先,父/子上下文不用于后台处理。它们用于可能在多个视图控制器中创建的相关数据的原子更新。因此,如果取消了最后一个视图控制器,则可以丢弃子上下文,而不会对父项产生负面影响。苹果在[^ 1]的答案底部对此进行了全面解释。现在已经不在了,而且你没有因为常见错误而堕落,你可以专注于如何正确地做背景核心数据。

创建一个新的持久性存储协调器(iOS 10上不再需要,请参阅下面的更新)和一个私有队列上下文。侦听保存通知并将更改合并到主上下文中(在iOS 10上,上下文具有自动执行此操作的属性)

对于Apple的样本,请参阅"地震:使用后台队列填充核心数据存储" https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html 从2014-08-19的修订历史中可以看出,他们补充说 "新示例代码,显示如何使用第二个Core Data堆栈获取后台队列上的数据。"

这是来自AAPLCoreDataStackManager.m的那一位:

// Creates a new Core Data stack and returns a managed object context associated with a private queue.
- (NSManagedObjectContext *)createPrivateQueueContext:(NSError * __autoreleasing *)error {

    // It uses the same store and model, but a new persistent store coordinator and context.
    NSPersistentStoreCoordinator *localCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[AAPLCoreDataStackManager sharedManager].managedObjectModel];

    if (![localCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil
                                                  URL:[AAPLCoreDataStackManager sharedManager].storeURL
                                              options:nil
                                                error:error]) {
        return nil;
    }

    NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
    [context performBlockAndWait:^{
        [context setPersistentStoreCoordinator:localCoordinator];

        // Avoid using default merge policy in multi-threading environment:
        // when we delete (and save) a record in one context,
        // and try to save edits on the same record in the other context before merging the changes,
        // an exception will be thrown because Core Data by default uses NSErrorMergePolicy.
        // Setting a reasonable mergePolicy is a good practice to avoid that kind of exception.
        context.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;

        // In OS X, a context provides an undo manager by default
        // Disable it for performance benefit
        context.undoManager = nil;
    }];
    return context;
}

在AAPLQuakesViewController.m

- (void)contextDidSaveNotificationHandler:(NSNotification *)notification {

    if (notification.object != self.managedObjectContext) {

        [self.managedObjectContext performBlock:^{
            [self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
        }];
    }
}

以下是样本设计的完整描述:

地震:使用"私人"持久性存储协调器以在后台获取数据

大多数使用Core Data的应用程序都使用单个持久性存储协调器来调解对给定持久性存储的访问。地震显示了如何使用额外的"私人"使用从远程服务器检索的数据创建托管对象时的持久性存储协调器。

应用程序架构

该应用程序使用两个核心数据"堆栈" (由持久性存储协调器的存在定义)。第一个是典型的"通用目的"堆;第二个是由视图控制器创建的,专门用于从远程服务器获取数据(从iOS 10开始,不再需要第二个协调器,请参阅答案底部的更新)。

主持久存储协调器由单独的堆栈控制器" object(CoreDataStackManager的一个实例)。客户有责任创建托管对象上下文以与协调器[^ 1]一起使用。堆栈控制器还会检查应用程序使用的托管对象模型的属性以及持久性存储的位置。客户端可以使用后面的这些属性来设置其他持久性存储协调器,以与主协调器并行工作。

主视图控制器是QuakesViewController的一个实例,它使用堆栈控制器的持久存储协调器从持久存储中获取地震以显示在表视图中。从服务器检索数据可以是长期运行的操作,其需要与持久存储进行大量交互以确定从服务器检索的记录是新地震还是对现有地震的潜在更新。为确保应用程序在此操作期间能够保持响应,视图控制器使用第二个协调器来管理与持久性存储的交互。它将协调器配置为使用相同的托管对象模型和持久存储作为堆栈控制器提供的主协调器。它创建一个绑定到私有队列的托管对象上下文,以从存储中获取数据并将更改提交给存储。

[^ 1]:这支持"传递接力棒"特别是在iOS应用程序中,上下文从一个视图控制器传递到另一个视图控制器。根视图控制器负责创建初始上下文,并在必要时将其传递给子视图控制器。

此模式的原因是确保对托管对象图的更改得到适当约束。核心数据支持"嵌套"托管对象上下文,允许灵活的体系结构,使其易于支持独立,可取消,更改集。使用子上下文,您可以允许用户对托管对象进行一组更改,然后可以将这些更改作为单个事务批量提交给父级(最终保存到存储),或者丢弃。如果应用程序的所有部分只是从应用程序委托中检索相同的上下文,则会使此行为难以或无法支持。

更新:在iOS 10中,Apple将同步从sqlite文件级别移至持久协调器。这意味着您现在可以创建一个私有队列上下文,并重用主上下文使用的现有协调器,而不会出现以前那样的性能问题,很酷!

答案 2 :(得分:4)

顺便说一下,这个问题的this repo非常清楚地解释了这个问题。以上任何感兴趣的Swift版本

let jsonArray = … //JSON data to be imported into Core Data
let moc = … //Our primary context on the main queue

let privateMOC = NSManagedObjectContext(concurrencyType: .PrivateQueueConcurrencyType)
privateMOC.parentContext = moc

privateMOC.performBlock {
    for jsonObject in jsonArray {
        let mo = … //Managed object that matches the incoming JSON structure
        //update MO with data from the dictionary
    }
    do {
        try privateMOC.save()
        moc.performBlockAndWait {
            do {
                try moc.save()
            } catch {
                fatalError("Failure to save context: \(error)")
            }
        }
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}

如果你在iOS 10及以上版本中使用document

,那就更简单了
let jsonArray = …
let container = self.persistentContainer
container.performBackgroundTask() { (context) in
    for jsonObject in jsonArray {
        let mo = CarMO(context: context)
        mo.populateFromJSON(jsonObject)
    }
    do {
        try context.save()
    } catch {
        fatalError("Failure to save context: \(error)")
    }
}