核心数据背景环境的最佳实践
我需要处理大量的核心数据。
假设我的核心数据模型如下所示:
Car ---- identifier type
我从我的服务器获取汽车信息JSON列表,然后我想与我的核心数据Car
对象同步,这意味着:
如果它的新车 – >从新的信息创build一个新的核心数据Car
对象。
如果汽车已经存在 – >更新Core Data Car
对象。
所以我想在后台做这个导入,而不会阻塞用户界面,而使用滚动汽车表格视图,呈现所有的汽车。
目前我正在做这样的事情:
// 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;
另一件我不确定的是什么时候调用保存在主要上下文中,在我的例子中,我只是调用保存在导入结束,但我看到的例子,使用:
[[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中的核心数据configuration:
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]; }]; } } }];
对于第一次接触Core Data的人来说,这是一个非常令人困惑的话题。 我不这么说,但有了经验,我有信心说苹果的文档在这个问题上有些误导(如果你仔细阅读,它实际上是一致的,但是没有充分说明合并数据的原因在许多情况下,比依靠父母/孩子的情况,并简单地将孩子从孩子保存到父母方面来说是更好的解决scheme。
文档给出了强烈的印象父/子上下文是进行后台处理的新的首选方式。 不过苹果忽视了一些强有力的警告。 首先,请注意,您获取到您的子上下文中的所有内容都将首先通过其父项。 因此,最好限制在主线程上运行的主要上下文的任何子项,以处理(编辑)已经在主线程的UI中呈现的数据。 如果将其用于常规同步任务,则可能需要处理的数据远远超出了当前在UI中显示的范围。 即使您使用NSPrivateQueueConcurrencyType,对于子编辑上下文,也可能会在主环境中拖动大量数据,这可能会导致性能不佳和阻塞。 现在最好不要将主要上下文作为用于同步的上下文的子元素,因为除非您要手动执行同步更新,否则不会收到同步更新的通知,此外,您还将执行潜在的长时间运行的任务上下文中,您可能需要响应从作为主要上下文的子级的编辑上下文(通过主联系人到数据存储区)级联发起的保存。 您将不得不手动合并数据,并且还可能跟踪主要上下文中需要失效的内容并重新同步。 不是最简单的模式。
苹果公司的文档没有说清楚的是,你最有可能需要混合使用描述“旧”线程约束方式的页面上描述的技术,以及新的父 – 子上下文方式。
您最好的select是(我在这里给出一个通用的解决scheme,最好的解决scheme可能取决于您的详细需求),有一个NSPrivateQueueConcurrencyType保存上下文作为最高的父,直接保存到数据存储。 [编辑:你不会直接在这方面做很多],然后至less给两个直接的孩子保存的情况下。 一个用于UI的NSMainQueueConcurrencyType主要上下文[编辑:最好是遵守规则,避免在这个上下文中对数据进行任何编辑],另一个是NSPrivateQueueConcurrencyType,用于编辑用户的数据,选项A在附图中)您的同步任务。
然后,将主上下文设置为由同步上下文生成的NSManagedObjectContextDidSave通知的目标,并将通知.userInfo字典发送到主上下文的mergeChangesFromContextDidSaveNotification :.
下一个要考虑的问题是放置用户编辑上下文的位置(用户编辑的上下文反映回界面的上下文)。 如果用户的操作总是局限于对less量呈现数据的编辑,那么使用NSPrivateQueueConcurrencyType再次将其作为主上下文的子元素是最好的select,并且最容易pipe理(保存将直接将编辑保存到主上下文中,如果你有一个NSFetchedResultsController,相应的委托方法将被自动调用,所以你的UI可以处理更新控制器:didChangeObject:atIndexPath:forChangeType:newIndexPath :)(这也是选项A)。
另一方面,如果用户操作可能会导致大量数据被处理,则可能需要考虑将其作为主上下文和同步上下文的另一个对等方,以使保存上下文具有三个直接子节点。 主 , 同步 (私人队列types)和编辑 (私人队列types)。 我已经在图上显示了这种安排作为选项B.
与同步上下文类似,在保存数据时(或者如果需要更多粒度,更新数据时),需要[编辑:configuration主上下文以接收通知],并采取操作合并数据(通常使用mergeChangesFromContextDidSaveNotification: )。 请注意,通过这种安排,主环境不需要调用save:方法。
为了理解父子关系,采取schemeA:父子方法只是意味着如果编辑上下文获取NSManagedObjects,那么它们将首先被保存上下文,然后被主要上下文“复制”(注册),最后编辑上下文。 您可以对其进行更改,然后当您在编辑上下文中调用save时,更改将保存到主上下文中 。 您将不得不在主要上下文中调用save:然后在保存上下文之前调用save:将其写入磁盘。
当你保存从一个孩子,直到父母,各种NSManagedObject的变化和保存通知被触发。 例如,如果您使用提取结果控制器来pipe理用户界面的数据,则会调用委托方法,以便可以根据需要更新UI。
一些后果:如果您在编辑上下文中获取对象和NSManagedObject A,然后修改它并保存,那么修改将返回到主上下文。 您现在已经修改了对象,同时针对主要和编辑上下文进行注册。 这样做会是一种糟糕的风格,但现在可以在主要上下文中再次修改该对象,现在它将与存储在编辑上下文中的对象不同。 如果您尝试对存储在编辑上下文中的对象进行进一步的修改,则修改将与主上下文中的对象不同步,并且任何保存编辑上下文的尝试都会引发错误。
因为这个原因,像选项A这样的安排,尝试获取对象,修改它们,保存它们并重置编辑上下文(例如[editContext reset]),只要运行循环的任何一次迭代(或任何给定的块传递给[editContext performBlock:])。也是最好的纪律处分,并避免在主要上下文做任何编辑。此外,重新迭代,因为所有处理主要是主线程,如果你取大量的对象到编辑上下文中,主要的上下文将在主线程上执行取指处理,因为这些对象是从父对象上下文迭代地拷贝的。如果有大量的数据被处理,这可能导致无响应所以如果你有一大堆被pipe理的对象,并且你有一个用户界面选项会导致它们全部被编辑,在这种情况下configuration你的应用程序就像选项A.这种情况选项B是更好的select。
如果你没有处理数千个对象,那么选项A可能是完全足够的。
顺便说一句,不要太担心你select的选项。 从A开始,如果你需要变成B,可能是一个好主意。比你想象的要容易做出这样的改变,通常后果比你想象的要less。
首先,父/子上下文不用于后台处理。 它们用于对可能在多个视图控制器中创build的相关数据进行primefaces更新。 所以如果最后一个视图控制器被取消,那么子上下文可以被抛弃,而不会对父对象产生不利的影响。 在[^ 1]这个答案的底部,苹果完全解释了这一点。 现在已经不存在这种常见错误,您可以关注如何正确地处理背景核心数据。
创build一个新的持久存储协调器(在iOS 10上不再需要,参见下面的更新)和一个私有队列上下文。 听取保存通知,并将更改合并到主要上下文中(在iOS 10上,上下文有自动执行此操作的属性)
有关Apple的示例,请参阅“地震:使用后台队列填充核心数据存储” https://developer.apple.com/library/mac/samplecode/Earthquakes/Introduction/Intro.html正如您从修订历史logging中看到的在2014-08-19他们添加了“新的示例代码,显示如何使用第二个核心数据堆栈来获取背景队列上的数据”。;
这是来自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]; }]; } }
以下是样本devise的完整描述:
地震:使用“专用”持久存储协调器在后台获取数据
大多数使用Core Data的应用程序使用单个持久存储协调器来调解对给定持久存储的访问。 当使用从远程服务器检索到的数据创build托pipe对象时,地震显示如何使用其他“专用”持久性存储协调器。
应用架构
应用程序使用两个核心数据“堆栈”(由存在持久性存储协调器定义)。 第一个是典型的“通用”堆栈; 第二个是由视图控制器专门从远程服务器获取数据创build的(从iOS 10开始,不再需要第二个协调器,请参阅答案底部的更新)。
主要的持久存储协调器由一个单独的“堆栈控制器”对象(CoreDataStackManager的一个实例)提供。 客户有责任创build一个托pipe对象上下文来与协调器[^ 1]一起工作。 堆栈控制器还为应用程序使用的受pipe对象模型和持久性存储的位置提供属性。 客户可以使用这些属性来设置额外的持久性商店协调员,与主协调员并行工作。
主视图控制器(QuakesViewController的一个实例)使用堆栈控制器的持久存储协调器从持久性存储中获取地震以显示在表格视图中。 从服务器检索数据可能是一个长期运行的操作,需要与持久性存储进行重要的交互,以确定从服务器检索到的logging是新的地震还是对现有地震的潜在更新。 为了确保应用程序在此操作期间保持响应,视图控制器使用第二个协调器来pipe理与持久性存储的交互。 它将协调器configuration为使用与堆栈控制器出售的主协调器相同的pipe理对象模型和持久性存储。 它创build一个绑定到专用队列的托pipe对象上下文,以便从存储中获取数据并将更改提交给存储。
[^ 1]:这支持“通过接力棒”的方法,尤其是在iOS应用程序中,上下文从一个视图控制器传递到另一个视图控制器。 根视图控制器负责创build初始上下文,并在必要时将其作为/传递给子视图控制器。
这种模式的原因是为了确保对被pipe理对象图的改变被适当地限制。 核心数据支持“嵌套的”pipe理对象上下文,允许灵活的架构,使支持独立的,可取消的变更集很容易。 通过子上下文,您可以允许用户对托pipe对象进行一系列更改,然后可以将其作为单个事务批量提交给父级(最终保存到商店),也可以丢弃。 如果应用程序的所有部分简单地从应用程序委托中检索相同的上下文,则会使此行为难以或不可能得到支持。
更新:在iOS 10中,Apple将同步从sqlite文件级别迁移到持久性协调器。 这意味着您现在可以创build一个私有队列上下文,并重用主环境使用的现有协调器,而不会像以前那样执行相同的性能问题,这很酷!
顺便说一句,苹果的这个文件很清楚地解释这个问题。 对于任何感兴趣的人来说,上面的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)") } }
如果您使用NSPersistentContainer for iOS 10及更高版本,甚至更简单
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)") } }