(CoreData) 如何高效地导入数据

Efficiently Importing Data

这篇文章讲述了如何更有效率地导入数据到Core Data应用并将数据转换为managed objects以持久化。并讨论了一些应该遵从的Cocoa基本模式,和一些专用于Core Data中的模式

Cocoa Fundamentals

一般说来,当你使用Core Data来导入数据文件时一定要记住一个在Cocoa 应用开发中需要应用的很重要的“一般规则”。如果你导入文件必须以某种方式解析时,这一般需要使用大量的临时文件。这会导致大量的内存占用甚至引起内存分页。就像没有使用Core Data的应用时会做的一样,你可以使用本地的自动释放池block来控制在内存存在对象数量。关于更多的Core Data与内存管理的交互参见 “Reducing Memory Overhead.”

你同时应该减少不必要的重复工作。在创建一个包含一个变量的predicate中有一个微妙的例子。如果像下面这样创建predicate里,你不仅仅是在每一个多循环都创建了一个predicate, 还同时在做解析。

// Loop over employeeIDs.
for (NSString *anID in employeeIDs) {
    NSString *predicateString = [NSString stringWithFormat:@"employeeID == %@", anID];
    NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];

为了从一个格式化的字符串中创建一个predicate, 框架必须解析这个字符串并创建一predicate的实例和一堆表达式对象。如果能使用同一个predicate对象,如果你是在重复使用同一种格式的predicate,只是在每次循环中改变表达式中的常量,你可以创建一个predicate的实例,然后每次再替换里面的常量 (参见 “Creating Predicates”),这样会更有效率. 这种技术如下面的代码所示。

NSString *predicateString = [NSString stringWithFormat @"employeeID == $EMPLOYEE_ID"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:predicateString];
for (NSString *anID in employeeIDs) {
    NSDictionary *variables = @{ @"EMPLOYEE_ID" : anID };
    NSPredicate *localPredicate = [predicate predicateWithSubstitutionVariables:variables];

Reducing Peak Memory Footprint

如果你需要导入大量的数据到Core Data应用中, 你应该批量导入这些数据并在批次间清理Core Data栈以保证降低应用的内存使用峰值。相关的案例与技术参见 “Core Data Performance” (特别是 “Reducing Memory Overhead”) 与 “Object Lifetime Management,” 方便起见这里总结了这些这些技术。

Importing in batches

首先,应该为导入创建多个分开的托管对象上Contexts,然后设置它们的undo manager为nil。 (Contexts 的创建并不是很耗资源,所以如果你缓存了 persistent store coordinator 你可以为了不同的工作集或特殊的操作使用不过的Contexts。)

NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] init];
NSPersistentStoreCoordinator *coordinator = <#Get the coordinator#>;
[importContext setPersistentStoreCoordinator:coordinator];
[importContext setUndoManager:nil];

(如你以有一个创建好的 Core Data 栈, 你可以从另外一个托管对象的context中获取 persistent store coordinator .) 设置undo manager 为 nil 意味着:

  1. 你不会把努力浪费在记录对不需要撤销的动作上(如插入);

  2. Undo manager 不会强引用被改变的对象以阻止他们被释放  (参见 “Change and Undo Management”).

你应该在分批导入数据差创建相应的托管对象 (最优的批次大小取决于每条记录的多大和你相要保持多低的内存占用). 在一个自动释放块中进行每一个批次的处理; 在每一个批量操之后你需要保存context (调用 save:). (直到保存前,context会对每一个未保存的改变保持强引用完成插入操作.)

Dealing with strong reference cycles

相互有引用的托管对象几互每次都会导致不可恢复的强引用环. 如果在导入中你在对象中创建了引用关系,你需要打破这个环来让对象在不再使用时得以析构。为了完成这个目标, you can either turn the objects into faults, or reset the whole context. 完整的讨论, 参见 “Breaking Relationship Strong Reference Cycles.”

Implementing Find-or-Create Efficiently

在导入数据中一个常用的技术是使用“查找或新增”模式, 来判断一个对象是否己经存在,如果没有则创建它。

为了一堆离散的值,在需要寻找己有对象(己经持久化)时你可能会遇到很多种情况. 一个简单的方法是创建一个循环, 然后对每一个值执行一次查询来判断是否己经存在一个匹配的对象. 这个模式不是很好。如果你分析使用这种模式的应用,你会发现每一次查询是在循环中最耗时的操作之一(相对于仅仅枚举集合中的每一个对象). 甚至这个模式会将问题的时间复杂度由 O(n) 变为 O(n^2)。

在可能的情况下有一个很高效的方法,一次性地创建所有托管对象,然后再修复对象间的关系。比如你导入的数据中不存在任何重复的数据(比如说你的初始集为空),你可以直接创建对应的托管对象而不用进行查询,或者你导入一个“展平”的数据,它们之音没有任何关联. 你可以先创建所有对应的托管对象,然后在保存前用一个大的 IN predicate来干掉重复的对象.

如果说你因为要导入一堆使用属相来相互关联的数据,在使用“查找或创建”模式时,你可以能过减少查询次数来优化查找己有对象的方式。完成这种方法取决于过程中有多少参考数据必须使用。如果你导入100个潜在的新对象,但在数据库中只有2000条数据,从数据库中取出所有的数据并缓存起来不会太蛋疼(特别是这些数据会反复用到)。但是如果你有 100,000 条数据在库中,把它们在内存中缓存起来就糟心了。

你可以在每个查询中组合使用IN predicate 和排序来减少 Core Data 的工作量. 举个例子,说你希望获先取得一个雇员ID(string类型)的列表, 然后再创建所有不在库中的雇员,or example, you want to take a list of employee IDs (as strings) and create Employee records for all those not already in the database. 考虑这段代码,雇员是一个有着 name 属性的实体, 一个 listOfIDsAsString 是一张雇员 ID 的表, 这里面的雇员如果没在库中的话你想把他们加入库中。

第一步,对相关的对象分离ID并排序

// get the names to parse in sorted order
NSArray *employeeIDs = [[listOfIDsAsString componentsSeparatedByString:@"\n"]
        sortedArrayUsingSelector: @selector(compare:)];

下一步,用 IN 名字字符串数 创建一个 predicate, 附带上一个排序描述来保证结果与名字的顺序一致. (IN 与 SQL中 IN 的操作一样, 都是判断左边的值一定会在右边的集合中出现.)

// Create the fetch request to get all Employees matching the IDs.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:
        [NSEntityDescription entityForName:@"Employee" inManagedObjectContext:aMOC]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat:@"(employeeID IN %@)", employeeIDs]];
// make sure the results are sorted as well
[fetchRequest setSortDescriptors:
        @[[[NSSortDescriptor alloc] initWithKey: @"employeeID" ascending:YES]]];

最后执行查询.

NSError *error;
NSArray *employeesMatchingNames = [aMOC executeFetchRequest:fetchRequest error:&error];

你会得到两个排序好的数组,一个里面有通查询的雇员ID,一个里面有对应的托管对象. 为了处理它们,可以用以下步聚遍历这两个数组:

  1. 取下一个 ID 和 雇员. 如果 ID 与  雇员 ID 不匹配, 创建一个与这个ID对应的新雇员。

  2. 取下一个雇员: 如果这个雇员的ID与刚才取到的ID匹配, 取下一个 ID 和 雇员.

不管你有多少个 ID 要处理, 你只需要一次查询,然后处理结果集即可。

下面的代码完整展示了上一段的例子。

// Get the names to parse in sorted order.
NSArray *employeeIDs = [[listOfIDsAsString componentsSeparatedByString:@"\n"]
        sortedArrayUsingSelector: @selector(compare:)];
// create the fetch request to get all Employees matching the IDs
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:
        [NSEntityDescription entityForName:@"Employee" inManagedObjectContext:aMOC]];
[fetchRequest setPredicate: [NSPredicate predicateWithFormat: @"(employeeID IN %@)", employeeIDs]];
// Make sure the results are sorted as well.
[fetchRequest setSortDescriptors:
    @[ [[NSSortDescriptor alloc] initWithKey: @"employeeID" ascending:YES] ]];
// Execute the fetch.
NSError *error;
NSArray *employeesMatchingNames = [aMOC executeFetchRequest:fetchRequest error:&error];

 

http://developer.apple.com/library/mac/#documentation/cocoa/conceptual/coredata/Articles/cdImporting.html#//apple_ref/doc/uid/TP40003174-SW4