核心数据背景上下文最佳实践 [英] Core Data background context best practice

查看:202
本文介绍了核心数据背景上下文最佳实践的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有一个大的导入任务,我需要做核心数据。

让我说的核心数据模型看起来像这样:

  Car 
----
identifier
type


b $ b

我从我的服务器获取汽车信息JSON列表,然后我要将其与我的核心数据 Car 对象同步,意味着:

如果是新车 - >从新信息中创建新的核心数据 Car 对象。

如果汽车已存在 - >更新核心数据 Car 对象。



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



目前我正在这样做:

  //创建后台上下文
NSManagedObjectContext * bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[bgContext setParentContext:self.mainContext];

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

//将新数据导入Core Data ...
//我试图在这里有效导入,
//尽可能少的提取,和批处理
for(... num of batches ...){

//做批量导入...

//保存bg上下文每个批处理结束
[bgContext save:& error];
}

//当所有导入批次结束时我在主上下文中调用保存

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

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

是否可以使用 setParentContext

我看到一些使用它的例子,但我看到的其他例子不调用 setParentContext ,而是他们做这样的事情:

  NSManagedObjectContext * bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; 
bgContext.persistentStoreCoordinator = self.mainContext.persistentStoreCoordinator;
bgContext.undoManager = nil;另一件我不确定的事情是,当调用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];
}];
}
}];

正如我之前提到的,我希望用户能够在更新时与数据进行交互,如果我在用户更改汽车类型,而导入更改相同的汽车,如果我写的安全吗?



更新: / h2>

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



这是AppDelegate中的Core Data配置:

  AppDelegate.m 

#pragma mark - Core数据栈

- (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;
}

//保存上下文,主上下文的父级
- (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];

//创建后台上下文
NSManagedObjectContext * bgContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
bgContext.parentContext = saveObjectContext;

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

//将新数据导入Core Data ...
//我试图在这里有效导入,
//尽可能少的提取,和批处理
for(... num of batches ...){

//做批量导入...

//保存bg上下文每个批处理结束
[bgContext save:& error];
}

//不调用这里的主保存...
//改为使用NSManagedObjectContextDidSaveNotification合并更改
}];
}

我还有以下观察者:



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


解决方案

这是一个非常混乱的主题,首次。我不会这么轻描淡写,但有经验,我有信心说,苹果文档在这个问题上有点误导(如果你仔细阅读它是一致的,但他们没有充分说明为什么合并数据保持在许多情况下比依赖父/子上下文和简单地从子节点保存到父节点更好的解决方案)。



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



苹果文档中没有明确的是,你最有可能需要一个混合的描述旧线程限制方式的页面描述的技术东西,和新的父 - 子上下文做事的方式。



你最好的赌注是(我在这里给出一个通用的解决方案,最好的解决方案可能取决于你的详细要求),有一个NSPrivateQueueConcurrencyType保存上下文最上层的父级,直接保存到数据存储区。 ,然后给保存上下文至少两个直接的孩子。一个你的NSMainQueueConcurrencyType主上下文你用于UI ,另一个NSPrivateQueueConcurrencyType,你用来做数据的用户编辑,



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



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



另一方面,如果用户操作可能导致处理大量数据,那么您可能需要考虑将其作为主上下文和同步上下文的另一个对等体,以便保存上下文有三个直接子项。 (私人队列类型)和 (私人队列类型)。我在图上显示了这个安排作为选项B.



与同步上下文类似,您需要在保存数据时(或者,如果您需要更详细的数据,更新),并采取操作来合并数据(通常使用mergeChangesFromContextDidSaveNotification :)。注意,使用这种安排,不需要主上下文来调用save:方法。



理解父/子关系,选项A:父子进程简单地意味着如果编辑上下文获取NSManagedObject,它们将首先被复制到(注册)保存上下文,然后是主上下文,然后最终编辑上下文。您可以对其进行更改,然后在编辑上下文中调用save:时,更改将保存到主上下文 。您必须在主上下文中调用save:,然后在保存上下文之前调用save:将它们写入磁盘。



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



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

因为这个原因,使用选项A的安排,尝试获取对象,修改它们,保存它们并重置编辑上下文是一个好的模式(例如[editContext reset]任何单循环的运行循环(或在任何给定的块传递给[editContext performBlock:])。最好是规范,避免对主上下文做任何
编辑
此外,为了重新迭代,由于main上的所有处理都是主线程,如果你为编辑上下文获取了大量对象,主上下文将在主线程上进行获取处理 因为这些对象被迭代地从父上下文复制到子上下文。如果有大量的数据被处理,这可能会导致UI无响应,所以如果你有一个大的被管理对象存储, UI选项,这将导致他们都被编辑。在这种情况下,配置您的应用程序像选项A是一个坏主意。在这种情况下,选项B是一个更好的赌注。



如果您没有处理数千个对象,则选项A可能完全足够。



BTW不要太担心您选择的选项。这可能是一个好主意,从A开始,如果你需要改变B.比你想象的更容易做这样的变化,通常有比你所期望的更少的后果。


I have a large import task I need to do with core data.
Let say my core data model look like this:

Car
----
identifier 
type

I fetch a list of car info JSON from my server and then I want to sync it with my core data Car object, meaning:
If its a new car -> create a new Core Data Car object from the new info.
If the car already exists -> update the Core Data Car object.

So I want to do this import in background without blocking the UI and while the use scrolls a cars table view that present all the cars.

Currently I'm doing something like this:

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

But I'm not really sure I'm doing the right thing here, for example:

Is it ok that I use setParentContext ?
I saw some examples that use it like this, but I saw other examples that don't call setParentContext, instead they do something like this:

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

Another thing that I'm not sure is when to call save on the main context, In my example I just call save in the end of the import, but I saw examples that uses:

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

As I mention before, I want the user to be able to interact with the data while updating, so what if I the user change a car type while the import change the same car, is the way I wrote it safe?

UPDATE:

Thanks to @TheBasicMind great explanation I'm trying to implement option A, so my code looks something like:

This is the Core Data configuration in 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;
}  

And this is how my import method looks like now:

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

And I also have the following observer:

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

解决方案

This is an extremely confusing topic for people approaching Core Data for the first time. I don't say this lightly, but with experience, I am confident in saying the Apple documentation is somewhat misleading on this matter (it is in fact consistent if you read it very carefully, but they don't adequately illustrate why merging data remains in many instances a better solution than relying on parent/child contexts and simply saving from a child to the parent).

The documentation gives the strong impression parent/child contexts are the new preferred way to do background processing. However Apple neglect to highlight some strong caveats. Firstly, be aware that everything you fetch into your child context is first pulled through it's parent. Therefore it is best to limit any child of the main context running on the main thread to processing (editing) data that has already been presented in the UI on the main thread. If you use it for general synchronisation tasks it is likely you will be wanting to process data which extends far beyond the bounds of what you are currently displaying in the UI. Even if you use NSPrivateQueueConcurrencyType, for the child edit context, you will potentially be dragging a large amount of data through the main context and that can lead to bad performance and blocking. Now it is best not to make the main context a child of the context you use for synchronisation, because it won't be notified of synchronisation updates unless you are going to do that manually, plus you will be executing potentially long running tasks on a context you might need to be responsive to saves initiated as a cascade from the edit context that is a child of your main context, through the main contact and down to the data store. You will have to either manually merge the data and also possibly track what needs to be invalidated in the main context and re-sync. Not the easiest pattern.

What the Apple documentation does not make clear is that you are most likely to need a hybrid of the techniques described on the pages describing the "old" thread confinement way of doing things, and the new Parent-Child contexts way of doing things.

Your best bet is probably (and I'm giving a generic solution here, the best solution may be dependent on your detailed requirements), to have a NSPrivateQueueConcurrencyType save context as the topmost parent, which saves directly to the datastore. [Edit: you won't be doing very much directly on this context], then give that save context at least two direct children. One your NSMainQueueConcurrencyType main context you use for the UI [Edit: it's best to be disciplined and avoid ever doing any editing of the data on this context], the other a NSPrivateQueueConcurrencyType, you use to do user edits of the data and also (in option A in the attached diagram) your synchronisation tasks.

Then you make the main context the target of the NSManagedObjectContextDidSave notification generated by the sync context, and send the notifications .userInfo dictionary to the main context's mergeChangesFromContextDidSaveNotification:.

The next question to consider is where you put the user edit context (the context where edits made by the user get reflected back into the interface). If the user's actions are always confined to edits on small amounts of presented data, then making this a child of the main context again using the NSPrivateQueueConcurrencyType is your best bet and easiest to manage (save will then save edits directly into the main context and if you have an NSFetchedResultsController, the appropriate delegate method will be called automatically so your UI can process the updates controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:) (again this is option A).

If on the other hand user actions might result in large amounts of data being processed, you might want to consider making it another peer of the main context and the sync context, such that the save context has three direct children. main, sync (private queue type) and edit (private queue type). I've shown this arrangement as option B on the diagram.

Similarly to the sync context you will need to [Edit: configure the main context to receive notifications] when data is saved (or if you need more granularity, when data is updated) and take action to merge the data in (typically using mergeChangesFromContextDidSaveNotification:). Note that with this arrangement, there is no need for the main context to ever call the save: method.

To understand parent/child relationships, take Option A: The parent child approach simply means if the edit context fetches NSManagedObjects, they will be "copied into" (registered with) first the save context, then the main context, then finally edit context. You will be able to make changes to them, then when you call save: on the edit context, the changes will saved just to the main context. You would have to call save: on the main context and then call save: on the save context before they will be written out to disk.

When you save from a child, up to a parent, the various NSManagedObject change and save notifications are fired. So for example if you are using a fetch results controller to manage your data for your UI, then it's delegate methods will be called so you can update the UI as appropriate.

Some consequences: If you fetch object and NSManagedObject A on the edit context, then modify it, and save, so the modifications are returned to the main context. You now have the modified object registered against both the main and the edit context. It would be bad style to do so, but you could now modify the object again on the main context and it will now be different from the object as it is stored in the edit context. If you then try to make further modifications to the object as stored in the edit context, your modifications will be out of sync with the object on the main context, and any attempt to save the edit context will raise an error.

For this reason, with an arrangement like option A, it is a good pattern to try to fetch objects, modify them, save them and reset the edit context (e.g. [editContext reset] with any single iteration of the run-loop (or within any given block passed to [editContext performBlock:]). It is also best to be disciplined and avoid ever doing any edits on the main context. Also, to re-iterate, since all processing on main is the main thread, if you fetch lots of objects to the edit context, the main context will be doing it's fetch processing on the main thread as those objects are being copied down iteratively from parent to child contexts. If there is a lot of data being processed, this can cause unresponsiveness in the UI. So if, for example you have a large store of managed objects, and you have a UI option that would result in them all being edited. It would be a bad idea in this case to configure your App like option A. In such a case option B is a better bet.

If you aren't processing thousands of objects, then option A may be entirely sufficient.

BTW don't worry too much over which option you select. It might be a good idea to start with A and if you need to change to B. It's easier than you might think to make such a change and usually has fewer consequences than you might expect.

这篇关于核心数据背景上下文最佳实践的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆