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


78

我有一个大型导入任务,我需要处理核心数据。
假设我的核心数据模型如下所示:

Car
----
identifier 
type

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

因此,我想在后台执行此导入操作而不阻塞UI,并且在用户滚动显示所有汽车的cars表视图时使用。

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

// 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];
        }];
    }
}];  

如前所述,我希望用户能够在更新时与数据进行交互,因此,如果我在导入更改同一辆汽车的同时更改了汽车类型,那么我写的方式安全吗?

更新:

感谢@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];
            }];
        }
    }
}];

如果使用父子模型,则必须先保存子模型,然后再保存父模型,以正确更新数据库。
carloabelli 2014年

这是我的问题的一部分,我应该为此类任务使用父子模型吗?我还有其他选择吗?
Eyal 2014年

1
您应该访问该帖子,您会发现它对您有帮助raywenderlich.com/15916/…–
Julio Montoya

1
@ cabellicar123为什么?从核心数据编程指南:并发:Once all of the data has been consumed and turned into NSManagedObject instances, you call save on the private context, which moves all of the changes into the main queue context without blocking the main queue. developer.apple.com/library/ios/documentation/Cocoa/Conceptual/...
迪马Deplov

@Eyal“我正在尝试在这里进行有效的导入”是什么感觉?

Answers:


184

对于初次接触Core Data的人们来说,这是一个极为混乱的话题。我不会轻易地说出来,但是根据经验,我有信心说Apple文档在此问题上有误导性(如果您仔细阅读,实际上是一致的,但是它们并不能充分说明为什么合并数据仍然存在在许多情况下,这是比依赖父/子上下文更好的解决方案,而仅仅是从子项保存到父项)。

该文档给人留下了深刻的印象,父/子上下文是进行后台处理的新首选方式。但是,苹果公司忽略了要强调的一些警告。首先,请注意,您获取到子上下文中的所有内容首先都会通过其父级。因此,最好将在主线程上运行的主上下文的任何子级限制为处理(编辑)已在主线程上的UI中显示的数据。如果将其用于常规同步任务,则可能要处理的数据远远超出了当前在UI中显示的范围。即使对子编辑上下文使用NSPrivateQueueConcurrencyType,也可能会在主上下文中拖动大量数据,这可能会导致性能下降和阻塞。现在最好不要将主上下文作为用于同步的上下文的子级,因为除非手动进行操作,否则不会收到同步更新的通知,此外,您还将在主线程上执行可能长时间运行的任务上下文,您可能需要对作为主上下文子级的编辑上下文,从主联系人一直到数据存储的级联启动的保存做出响应。您将不得不手动合并数据,还可能跟踪在主上下文中需要无效的内容并重新同步。不是最简单的模式。另外,您将在一个上下文中执行可能会长时间运行的任务,您可能需要响应从作为主上下文的子级的编辑上下文到主联系人再到数据存储的级联启动的保存。您将不得不手动合并数据,还可能跟踪在主上下文中需要无效的内容并重新同步。不是最简单的模式。另外,您将在一个上下文中执行可能会长时间运行的任务,您可能需要响应从作为主上下文的子级的编辑上下文到主联系人再到数据存储的级联启动的保存。您将不得不手动合并数据,还可能跟踪在主上下文中需要无效的内容并重新同步。不是最简单的模式。

Apple文档未明确指出的是,您最有可能需要混合使用描述页面上描述的“旧的”线程限制做事方式和新的Parent-Child上下文做事方式的技术。

最好的选择是(我在这里给出一个通用的解决方案,最好的解决方案可能取决于您的详细要求),使NSPrivateQueueConcurrencyType将上下文保存为最高父级,并将其直接保存到数据存储中。[编辑:您将不会直接在此上下文上做很多事情],然后为该保存上下文至少提供两个直接子代。一个用于UI的NSMainQueueConcurrencyType主上下文[编辑:最好受到约束,并避免在此上下文上进行任何数据编辑],另一个NSPrivateQueueConcurrencyType用于您对数据进行用户编辑,并且(在附件中的选项A)同步任务。

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

下一个要考虑的问题是将用户编辑上下文(用户所做的编辑反映到界面中的上下文)放在哪里。如果用户的操作始终只限于对少量呈现的数据进行编辑,那么最好使用NSPrivateQueueConcurrencyType将其作为主上下文的子对象,这是您最好的选择,也是最容易管理的操作(保存将直接将编辑内容保存到主上下文中,如果如果您有一个NSFetchedResultsController,则会自动调用相应的委托方法,以便您的UI可以处理更新controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:)(同样,这是选项A)。

另一方面,如果用户操作可能导致要处理大量数据,则可能要考虑使其成为主上下文和同步上下文的另一个对等方,这样保存上下文便具有三个直接子级。mainsync(专用队列类型)和edit(专用队列类型)。我在图上将这种安排显示为选项B。

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

要了解父子关系,请采用选项A:父子方法仅表示如果编辑上下文获取NSManagedObjects,将首先将它们“复制”到(注册)保存上下文,然后是主上下文,然后是“复制”到最后,然后是编辑上下文。您将能够对其进行更改,然后在调用save时:在编辑上下文中,所做的更改将保存到主上下文中。您必须先在主上下文上调用save:,然后在save上下文上调用save:,然后将它们写出到磁盘。

从子级保存到父级保存时,将触发各种NSManagedObject更改和保存通知。因此,例如,如果您使用提取结果控制器来管理UI数据,则将调用其委托方法,以便您可以适当地更新UI。

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

因此,使用选项A之类的安排,尝试获取对象,对其进行修改,保存它们并重置编辑上下文(例如,使用运行循环的任何单个迭代(例如,在[editContext reset]中,传递给[editContext performBlock:]的任何给定块。最好也要进行纪律,并避免对主上下文进行任何编辑。此外,由于要对main进行的所有处理都是主线程,因此请重申一下编辑上下文有很多对象,主要上下文将在获取过程中进行在主线程上进行因为这些对象是从父上下文到子上下文以迭代方式向下复制的。如果要处理大量数据,这可能会导致UI无响应。因此,例如,如果您拥有大量的托管对象,并且有一个UI选项将导致它们全部被编辑。在这种情况下,像选项A那样配置您的App是一个坏主意。在这种情况下,选项B是更好的选择。

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

顺便说一句,您不必担心选择哪个选项。从A开始,如果需要更改为B,则是一个好主意。进行此类更改比您想像的要容易,并且通常产生的后果比您预期的要少。


感谢您(和+1)的详细回答,在我的开发阶段,添加这些额外的上下文将是一个很大的变化。我真的只想使用一个额外的上下文来执行此后台任务并使其尽可能简单。我仍然无法理解亲子方法与其他方法之间的区别。如果您能使用我的示例代码并修复在此所做的任何错误步骤,我将不胜感激,这对于我来说更容易理解和执行。
2014年

采用选项A。父子方法仅表示如果编辑上下文获取NSManagedObjects,将首先将它们“复制”到(注册)保存上下文,然后复制到主上下文,然后再“复制”到该上下文。您将能够对其进行更改,然后在调用save时:在编辑上下文中,所做的更改将保存到主上下文中。您必须先在主上下文中调用save :,然后在保存中调用save :,然后将它们写出到磁盘。我将修改我的回复。
TheBasicMind 2014年

3
很高兴您决定在这里回答,您的信息是无价的。我想我现在有更多的信心去尝试一下,我只需要对我脑海中所有这些很棒的信息做一些整理即可。编辑上下文使我有些困惑,它与任何其他背景上下文之间的区别是什么?为什么他是个孩子而同步上下文不是?无论如何,现在我认为我不会使用单独的上下文来进行用户编辑,因为用户一次只能删除一个项目(汽车)或一次更改一个属性,因此我使用主上下文进行每个编辑并直接调用保存:就可以了。
2014年

我还使用为实现选项A(减去Edit上下文子代)而编写的一些新代码更新了我的问题。您能否检查是否还可以,我不确定是否需要在“保存”上下文中的任何地方调用save:还是足以将通知中的更改合并?
2014年

@TheBasicMind什么时候保存“保存”上下文?您是否独立于用户交互而这样做?
ctietze

14

首先,父/子上下文不用于后台处理。它们用于可能在多个视图控制器中创建的相关数据的原子更新。因此,如果取消了最后一个视图控制器,则可以丢弃子上下文,而不会对父上下文造成不利影响。苹果对此答案底部的[^ 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开始,不再需要第二个协调器,请参见答案底部的更新)。

主要的持久性存储协调器由单例“堆栈控制器”对象(CoreDataStackManager的一个实例)提供。客户的责任是创建与协调器[^ 1]一起使用的托管对象上下文。堆栈控制器还为应用程序使用的托管对象模型提供属性,以及永久存储的位置。客户可以使用这些后者的属性来设置其他持久性存储协调器,以与主协调器并行工作。

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

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

这种模式的原因是要确保对受管理对象图的更改受到适当的约束。核心数据支持“嵌套”管理对象上下文,这提供了灵活的体系结构,可轻松支持独立的,可取消的变更集。在子上下文中,您可以允许用户对托管对象进行一组更改,然后将这些更改作为单个事务批量提交给父对象(并最终保存到商店)或丢弃。如果应用程序的所有部分仅从例如应用程序委托中检索相同的上下文,则将使这种行为难以或不可能得到支持。

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


7

顺便说一下,苹果的这份文件正在非常清楚地解释这个问题。以上任何人的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及更高版本的NSPersistentContainer,则更加简单

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)")
    }
}

如何获取数据,您能举个例子吗,因为获取时我会随机崩溃,有时会正常工作。@hariszaman
Parth Barot

可能是您正在尝试同时修改和保存上下文
hariszaman19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.