coreData详解
1、初识CoreData
CoreData的结构构成:

NSManagedObjectModel的构成:

可以通过Entity创建继承自NSManagedObject类的文件,这个文件就是开发中使用的托管对象,具备模型对象的表示功能,CoreData的本地持久化都是通过这个类及其子类完成的。
在CoreData的整体结构中,主要分为两部分。一个是NSManagedObjectContext管理的模型部分,管理着所有CoreData的托管对象。一个是SQLite实现的本地持久化部分,负责和SQL数据库进行数据交互,主要由NSPersistentStore类操作。这就构成了CoreData的大体结构。

从图中可以看出,这两部分都是比较独立的,两部分的交互由一个持久化存储调度器(NSPersistentStoreCoordinator)来控制。上层NSManagedObjectContext存储的数据都是交给持久化调度器,由调度器调用具体的持久化存储对象(NSPersistentStore)来操作对应的数据库文件,NSPersistentStore负责存储的实现细节。这样就很好的将两部分实现了分离。
2、认识CoreData-基础使用
在模型文件的实体中,参数类型和平时创建继承自NSObject的模型类大体类似,但是还是有一些关于类型的说明,下面简单的列举了一下。
-
Undefined: 默认值,参与编译会报错
-
Integer 16: 整数,表示范围
-32768 ~ 32767 -
Integer 32: 整数,表示范围
-2147483648 ~ 2147483647 -
Integer 64: 整数,表示范围
–9223372036854775808 ~ 9223372036854775807 -
Float: 小数,通过
MAXFLOAT宏定义来看,最大值用科学计数法表示是0x1.fffffep+127f -
Double: 小数,小数位比
Float更精确,表示范围更大 -
String: 字符串,用
NSString表示 -
Boolean: 布尔值,用
NSNumber表示 -
Date: 时间,用
NSDate表示 -
Binary Data: 二进制,用
NSData表示 -
Transformable:
OC对象,用id表示。可以在创建托管对象类文件后,手动改为对应的OC类名。使用的前提是,这个OC对象必须遵守并实现NSCoding协议
在实体最下面,有一个Fetched Properties选项,这个选项用的不多,这里就不细讲了。Fetched Properties用于定义查询操作,和NSFetchRequest功能相同。定义fetchedProperty对象后,可以通过NSManagedObjectModel类的fetchRequestFromTemplateWithName:substitutionVariables:方法或其他相关方法获取这个fetchedProperty对象。

获取这个对象后,系统会默认将这个对象缓存到一个字典中,缓存之后也可以通过fetchedProperty字典获取fetchedProperty对象。
属性设置:
-
default Value: 设置默认值,除了二进制不能设置,其他类型几乎都能设置。
-
optional: 在使用时是否可选,也可以理解为如果设置为
NO,只要向MOC进行save操作,这个属性是否必须有值。否则MOC进行操作时会失败并返回一个error,该选项默认为YES。 -
transient: 设置当前属性是否只存在于内存,不被持久化到本地,如果设置为
YES,这个属性就不参与持久化操作,属性的其他操作没有区别。transient非常适合存储一些在内存中缓存的数据,例如存储临时数据,这些数据每次都是不同的,而且不需要进行本地持久化,所以可以声明为transient的属性。 -
indexed: 设置当前属性是否是索引。添加索引后可以有效的提升检索操作的速度。但是对于删除这样的操作,删除索引后其他地方还需要做出相应的变化,所以速度会比较慢。
-
Validation: 通过
Validation可以设置Max Value和Min Value,通过这两个条件来约定数据,对数据的存储进行一个验证。数值类型都有相同的约定方式,而字符串则是约定长度,date是约定时间。 -
Reg. Ex.(
Regular Expression): 可以设置正则表达式,用来验证和控制数据,不对数据自身产生影响。(只能应用于String类型) -
Allows External Storage: 当存储二进制文件时,如果遇到比较大的文件,是否存储在存储区之外。如果选择
YES,存储文件大小超过1MB的文件,都会存储在存储区之外。否则大型文件存储在存储区内,会造成SQLite进行表操作时,效率受到影响。
Relationships设置:
-
delete rule: 定义关联属性的删除规则。在当前对象和其他对象有关联关系时,当前对象被删除后与之关联对象的反应。这个参数有四个枚举值,代码对应着模型文件的相同选项。
NSNoActionDeleteRule 删除后没有任何操作,也不会将关联对象的关联属性指向
nil。删除后使用关联对象的关联属性,可能会导致其他问题。NSNullifyDeleteRule 删除后会将关联对象的关联属性指向
nil,这是默认值。NSCascadeDeleteRule 删除当前对象后,会将与之关联的对象也一并删除。
NSDenyDeleteRule 在删除当前对象时,如果当前对象还指向其他关联对象,则当前对象不能被删除。
-
Type: 主要有两种类型,
To One和To Many,表示当前关系是一对多还是一对一。
实体:
-
Parent Entity: 可以在实体中创建继承关系,在一个实体的菜单栏中通过
Parent Entity可以设置父实体,这样就存在了实体的继承关系,最后创建出来的托管模型类也是具有继承关系的。注意继承关系中属性名不要相同。
使用了这样的继承关系后,系统会将子类继承父类的数据,存在父类的表中,所有继承自同一父类的子类都会将父类部分存放在父类的表中。这样可能会导致父类的表中数据量过多,造成性能问题。
2、CoreData-基础使用
Fetched Properties
在实体最下面,有一个Fetched Properties选项,这个选项用的不多,这里就不细讲了。Fetched Properties用于定义查询操作,和NSFetchRequest功能相同。定义fetchedProperty对象后,可以通过NSManagedObjectModel类的fetchRequestFromTemplateWithName:substitutionVariables:方法或其他相关方法获取这个fetchedProperty对象。

获取这个对象后,系统会默认将这个对象缓存到一个字典中,缓存之后也可以通过fetchedProperty字典获取fetchedProperty对象。
Fetch Requests
在模型文件中Entities下面有一个Fetch Requests,这个也是配置请求对象的。但是这个使用起来更加直观,可以很容易的完成一些简单的请求配置。相对于上面讲到的Fetched Properties,这个还是更方便使用一些。

上面是对Employee实体的height属性配置的Fetch Request,这里配置的height要小于2米。配置之后可以通过NSManagedObjectModel类的fetchRequestTemplateForName:方法获取这个请求对象,参数是这个请求配置的名称,也就是EmployeeFR。
CoreData增删改查
- (IBAction)SchoolAdd:(UIButton *)sender { // 创建托管对象,并指明创建的托管对象所属实体名 _student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext]; _student.name = @"lxz"; // 实体中所有基础数据类型,创建类文件后默认都是NSNumber类型的 _student.age = @(23); // 通过上下文保存对象,并在保存前判断是否有更改 NSError * error = nil; if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) { BOOL isAddSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error]; if (isAddSuccess) { NSLog(@"SchoolisAddSuccess"); } } // 错误处理,可以在这实现自己的错误处理逻辑 if (error) { NSLog(@"CoreData Insert Data Error : %@", error); } }
- (IBAction)SchoolDelete:(UIButton *)sender { // 建立获取数据的请求对象,指明对Student实体进行删除操作 NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 创建谓词对象,过滤出符合要求的对象,也就是要删除的对象 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"]; // 执行获取操作,找到要删除的对象 NSError * error = nil; NSArray<Student *> * students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历符合删除要求的对象数组,执行删除操作 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext deleteObject:obj]; }]; // 保存上下文,并判断当前上下文是否有改动 if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) { BOOL isDeleteSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:nil]; if (isDeleteSuccess) { NSLog(@"SchoolisDeleteSuccess"); } } // 错误处理 if (error) { NSLog(@"CoreData Delete Data Error : %@", error); } }
- (IBAction)SchoolUpdate:(UIButton *)sender { // 建立获取数据的请求对象,并指明操作的实体为Student NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 创建谓词对象,设置过滤条件 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"]; request.predicate = predicate; // 执行获取请求,获取到符合要求的托管对象 NSError * error = nil; NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历获取到的数组,并执行修改操作 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { obj.age = @(24); }]; // 将上面的修改进行存储 if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) { BOOL isUpdateSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:nil]; if (isUpdateSuccess) { NSLog(@"SchoolIsUpdateSuccess"); } } // 错误处理 if (error) { NSLog(@"CoreData Update Data Error : %@", error); } /** 在上面简单的设置了NSPredicate的过滤条件,对于比较复杂的业务需求,还可以设置复合过滤条件,例如下面的例子 [NSPredicate predicateWithFormat:@"(age < 25) AND (firstName = XiaoZhuang)"] 也可以通过NSCompoundPredicate对象来设置复合过滤条件 [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[predicate1, predicate2]] */ }
- (IBAction)SchoolSearch:(UIButton *)sender { // 建立获取数据的请求对象,指明操作的实体为Student NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 执行获取操作,获取所有Student托管对象 NSError * error = nil; NSArray<Student *> * students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历输出查询结果 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Student Name : %@, Age : %d", obj.name, obj.age); }]; // 错误处理 if (error) { NSLog(@"CoreData Ergodic Data Error : %@", error); } }
3、CoreData-使用进阶
CoreData中可以通过设置NSFetchRequest类的predicate属性,来设置一个NSPredicate类型的谓词对象当做过滤条件。通过设置这个过滤条件,可以只获取符合过滤条件的托管对象,不会将所有托管对象都加载到内存中。这样是非常节省内存和加快查找速度的,设计一个好的NSPredicate可以优化CoreData搜索性能。
[NSPredicate predicateWithFormat:@"age >= 30"]
可以通过NSPredicate对iOS中的集合对象执行过滤操作,可以是NSArray、NSSet及其子类。对不可变数组NSArray执行的过滤,过滤后会返回一个NSArray类型的结果数组,其中存储着符合过滤条件的对象。
NSArray *results = [array filteredArrayUsingPredicate:predicate]
谓词不只可以过滤简单条件,还可以过滤复杂条件,设置复合过滤条件。
[NSPredicate predicateWithFormat:@"(age < 25) AND (firstName = XiaoZhuang)"]
当然也可以通过NSCompoundPredicate对象来设置复合过滤条件,返回结果是一个NSPredicate的子类NSCompoundPredicate对象。
[[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[predicate1, predicate2]]
NSPredicate中还可以使用正则表达式,可以通过正则表达式完成一些复杂需求,这使得谓词的功能更加强大,例如下面是一个手机号验证的正则表达式。
NSString *mobile = @"^1(3[0-9]|5[0-35-9]|8[025-9])\\d{8}$"; NSPredicate *regexmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", mobile];
NSPredicate支持对数据的模糊查询,例如下面使用通配符来匹配包含lxz的结果,具体CoreData中的使用在下面会讲到。
[NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]
NSPredicate在创建查询条件时,还支持设置被匹配目标的keyPath,也就是设置更深层被匹配的目标。例如下面设置employee的name属性为查找条件,就是用点语法设置的keyPath。
[NSPredicate predicateWithFormat:@"employee.name = %@", @"lxz"]
在执行fetch操作前,可以给NSFetchRequest设置一些参数,这些参数包括谓词、排序等条件,下面是一些基础的设置。
- 设置查找哪个实体,从数据库的角度来看就是查找哪张表,通过
fetchRequestWithEntityName:或初始化方法来指定表名。 - 通过
NSPredicate类型的属性,可以设置查找条件,这个属性在开发中用得最多。NSPredicate可以包括固定格式的条件以及正则表达式。 - 通过
sortDescriptors属性,可以设置获取结果数组的排序方式,这个属性是一个数组类型,也就是可以设置多种排序条件。(但是注意条件不要冲突) - 通过
fetchOffset属性设置从查询结果的第几个开始获取,通过fetchLimit属性设置每次获取多少个。主要用于分页查询,后面会讲。
MOC执行fetch操作后,获取的结果是以数组的形式存储的,数组中存储的就是托管对象。NSFetchRequest提供了参数resultType,参数类型是一个枚举类型。通过这个参数,可以设置执行fetch操作后返回的数据类型。
设置获取条件
// 建立获取数据的请求对象,并指明操作Employee表 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"]; // 设置请求条件,通过设置的条件,来过滤出需要的数据 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"]; request.predicate = predicate; // 设置请求结果排序方式,可以设置一个或一组排序方式,最后将所有的排序方式添加到排序数组中 NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES]; // NSSortDescriptor的操作都是在SQLite层级完成的,不会将对象加载到内存中,所以对内存的消耗是非常小的 request.sortDescriptors = @[sort]; // 执行获取请求操作,获取的托管对象将会被存储在一个数组中并返回 NSError *error = nil; NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error]; [employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Employee Name : %@, Height : %@, Brithday : %@", obj.name, obj.height, obj.brithday); }]; // 错误处理 if (error) { NSLog(@"CoreData Fetch Data Error : %@", error); }
这里设置NSFetchRequest对象的一些请求条件,设置查找Employee表中name为lxz的数据,并且将所有符合的数据用height值升序的方式排列。
查询操作
// 创建获取数据的请求对象,并指明操作Department表 NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Department"]; // 设置请求条件,设置employee的name为请求条件。NSPredicate的好处在于,可以设置keyPath条件 NSPredicate *predicate = [NSPredicate predicateWithFormat:@"employee.name = %@", @"lxz"]; request.predicate = predicate; // 执行查找操作 NSError *error = nil; NSArray<Department *> *departments = [context executeFetchRequest:request error:&error]; [departments enumerateObjectsUsingBlock:^(Department * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Department Search Result DepartName : %@, employee name : %@", obj.departName, obj.employee.name); }]; // 错误处理 if (error) { NSLog(@"Department Search Error : %@", error); }
查找Department实体,并打印实体内容。就像上面讲的双向关系一样,有关联关系的实体,自己被查找出来后,也会将与之关联的其他实体也查找出来,并且查找出来的实体都是关联着MOC的。
分页查询
在从本地存储区获取数据时,可以指定从第几个获取,以及本次查询获取多少个数据,联合起来使用就是分页查询。当然也可以根据需求,单独使用这两个API。
这种需求在实际开发中非常常见,例如TableView中,上拉加载数据,每次加载20条数据,就可以利用分页查询轻松实现。
#pragma mark - ----- Page && Fuzzy ------ //分页查询 - (IBAction)pageSearch:(UIButton *)sender { // 创建获取数据的请求对象,并指明操作Student表 NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 设置查找起始点,这里是从搜索结果的第六个开始获取 request.fetchOffset = 6; // 设置分页,每次请求获取六个托管对象 request.fetchLimit = 6; // 设置排序规则,这里设置年龄升序排序 NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]; request.sortDescriptors = @[descriptor]; // 执行查询操作 NSError * error = nil; NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历输出查询结果 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Page Search Result Name : %@, Age : %d", obj.name, obj.age); }]; // 错误处理 if (error) { NSLog(@"Page Search Data Error : %@", error); } }
上面是一个按照身高升序排序,分页获取搜索结果的例子。查找Employee表中的实体,将结果按照height字段升序排序,并从结果的第六个开始查找,并且设置获取的数量也是六个。
模糊查询
有时需要获取具有某些相同特征的数据,这样就需要对查询的结果做模糊匹配。在CoreData执行模糊匹配时,可以通过NSPredicate执行这个操作。
//模糊查询 - (IBAction)fuzzySearch:(UIButton *)sender { // 创建获取数据的请求对象,设置对Student表进行操作 NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 创建模糊查询条件。这里设置的带通配符的查询,查询条件是结果包含lxz NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]; request.predicate = predicate; // 执行查询操作 NSError * error = nil; NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历输出查询结果 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Fuzzy Search Result Name : %@, Age : %d", obj.name, obj.age); }]; // 错误处理 if (error) { NSLog(@"Fuzzy Search Data Error : %@", error); } /** 模糊查询的关键在于设置模糊查询条件,除了上面的模糊查询条件,还可以设置下面三种条件 */ // 以lxz开头 // NSPredicate *predicate1 = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", @"lxz"]; // 以lxz结尾 // NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"name ENDSWITH %@" , @"lxz"]; // 其中包含lxz // NSPredicate *predicate3 = [NSPredicate predicateWithFormat:@"name contains %@" , @"lxz"]; // 还可以设置正则表达式作为查找条件,这样使查询条件更加强大,下面只是给了个例子 // NSString *mobile = @"^1(3[0-9]|5[0-35-9]|8[025-9])\\d{8}$"; // NSPredicate *predicate4 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", mobile]; }
上面是使用通配符的方式进行模糊查询,NSPredicate支持多种形式的模糊查询,下面列举一些简单的匹配方式。模糊查询条件对大小写不敏感,所以查询条件大小写均可。
加载请求模板
在之前的文章中谈到在模型文件中设置请求模板,也就是在.xcdatamodeld文件中,设置Fetch Requests,使用时可以通过对应的NSManagedObjectModel获取设置好的模板。
#pragma mark - ----- Fetch Request ------ /** 加载模型文件中设置的FetchRequest请求模板,模板名为StudentAge,在School.xcdatamodeld中设置 */ - (IBAction)fetchRequest:(UIButton *)sender { // 通过MOC获取托管对象模型,托管对象模型相当于.xcdatamodeld文件,存储着.xcdatamodeld文件的结构 NSManagedObjectModel * model = [CoreDataManager sharedCoreDataManager].persistentContainer.managedObjectModel; // 通过.xcdatamodeld文件中设置的模板名,获取请求对象 NSFetchRequest * fetchRequest = [model fetchRequestTemplateForName:@"StudentAge"]; // 请求数据,下面的操作和普通请求一样 NSError *error = nil; NSArray<Student *> *dataList = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error]; // 遍历获取结果,并打印结果 [dataList enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Student.count = %ld, Student.age = %d", dataList.count, obj.age); }]; // 错误处理 if (error) { NSLog(@"Execute Fetch Request Error : %@", error); } }
请求结果排序
/** 对请求结果进行排序 这个排序是发生在数据库一层的,并不是将结果取出后排序,所以效率比较高 */ - (IBAction)resultSort:(UIButton *)sender { // 建立获取数据的请求对象,并指明操作Student表 NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 设置请求条件,通过设置的条件,来过滤出需要的数据 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]; request.predicate = predicate; // 设置请求结果排序方式,可以设置一个或一组排序方式,最后将所有的排序方式添加到排序数组中 NSSortDescriptor * sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]; // NSSortDescriptor的操作都是在SQLite层级完成的,不会将对象加载到内存中,所以对内存的消耗是非常小的 // 下面request的sort对象是一个数组,也就是可以设置多种排序条件,但注意条件不要冲突 request.sortDescriptors = @[sort]; // 执行获取请求操作,获取的托管对象将会被存储在一个数组中并返回 NSError * error = nil; NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error]; // 遍历返回结果 [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"Employee Name : %@, Age : %d", obj.name, obj.age); }]; // 错误处理 if (error) { NSLog(@"CoreData Fetch Data Error : %@", error); } }
获取结果Count值
开发过程中有时需要只获取所需数据的Count值,也就是执行获取操作后数组中所存储的对象数量。遇到这个需求,如果像之前一样MOC执行获取操作,获取到数组然后取Count,这样对内存消耗是很大的。
对于这个需求,苹果提供了两种常用的方式获取这个Count值。这两种获取操作,都是在数据库中完成的,并不需要将托管对象加载到内存中,对内存的开销也是很小的。
方法1,设置resultType
/** 获取返回结果的Count值,通过设置NSFetchRequest的resultType属性 */ - (IBAction)getResultCount1:(UIButton *)sender { // 设置过滤条件,可以根据需求设置自己的过滤条件 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < 24"]; // 创建请求对象,并指明操作Student表 NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; fetchRequest.predicate = predicate; // 这一步是关键。设置返回结果类型为Count,返回结果为NSNumber类型 fetchRequest.resultType = NSCountResultType; // 执行查询操作,返回的结果还是数组,数组中只存在一个对象,就是计算出的Count值 NSError * error = nil; NSArray * dataList = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error]; // 返回结果存在数组的第一个元素中,是一个NSNumber的对象,通过这个对象即可获得Count值 NSInteger count = [dataList.firstObject integerValue]; NSLog(@"fetch request result Employee.count = %ld", count); // 错误处理 if (error) { NSLog(@"fetch request result error : %@", error); } }
方法1中设置NSFetchRequest对象的resultType为NSCountResultType,获取到结果的Count值。这个枚举值在之前的文章中提到过,除了Count参数,还可以设置其他三种参数。
方法2,使用MOC提供的方法
- (IBAction)getResultCount2:(UIButton *)sender { // 设置过滤条件 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < 24"]; // 创建请求对象,指明操作Student表 NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; fetchRequest.predicate = predicate; // 通过调用MOC的countForFetchRequest:error:方法,获取请求结果count值,返回结果直接是NSUInteger类型变量 NSError * error = nil; NSUInteger count = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext countForFetchRequest:fetchRequest error:&error]; NSLog(@"fetch request result count is : %ld", count); // 错误处理 if (error) { NSLog(@"fetch request result error : %@", error); } }
MOC提供了专门获取请求结果Count值的方法,通过这个方法可以直接返回一个NSUInteger类型的Count值,使用起来比上面的方法更方便点,其他都是一样的。
位运算
假设有需求是对Employee表中,所有托管对象的height属性计算总和。这个需求在数据量比较大的情况下,将所有托管对象加载到内存中是非常消耗内存的,就算批量加载也比较耗时耗内存。
CoreData对于这样的需求,提供了位运算的功能。MOC在执行请求时,是支持对数据进行位运算的。这个操作依然是在数据库层完成的,对内存的占用非常小。
/** 对返回的结果进行按位运算,这个运算是发生在SQLite数据库层的,所以执行效率很快,对内存的消耗也很小 如果需要对托管对象的某个属性进行运算,比较推荐这种效率高的方法. */ - (IBAction)bitwiseArithmetic:(UIButton *)sender { // 创建请求对象,指明操作Student表 NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 设置返回值为字典类型,这是为了结果可以通过设置的name名取出,这一步是必须的 fetchRequest.resultType = NSDictionaryResultType; // 创建描述对象的name字符串 NSString * descriptionName = @"sumOperatin"; // 创建描述对象 NSExpressionDescription * expressionDes = [[NSExpressionDescription alloc] init]; // 设置描述对象的name,最后结果需要用这个name当做key来取出结果 expressionDes.name = descriptionName; // 设置返回值类型,根据运算结果设置类型 expressionDes.expressionResultType = NSInteger16AttributeType; // 创建具体描述对象,用来描述对哪个属性进行什么运算(可执行的运算类型很多,这里描述的是对age属性,做sum运算) NSExpression * expression = [NSExpression expressionForFunction:@"sum:" arguments:@[[NSExpression expressionForKeyPath:@"age"]]]; // 只能对应一个具体描述对象 expressionDes.expression = expression; // 给请求对象设置描述对象,这里是一个数组类型,也就是可以设置多个描述对象 fetchRequest.propertiesToFetch = @[expressionDes]; // 执行请求,返回值还是一个数组,数组中只有一个元素,就是存储计算结果的字典 NSError * error = nil; NSArray * resultArr = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error]; // 通过上面设置的name值,当做请求结果的key取出计算结果 NSNumber * number = resultArr.firstObject[descriptionName]; NSLog(@"fetch request result is %ld", [number integerValue]); // 错误处理 if (error) { NSLog(@"fetch request result error : %@", error); } /** 位运算支持的算法种类很多,具体可以在NSExpression.h文件中查看 */ }

从执行结果可以看到,MOC对所有查找到的托管对象height属性执行了求和操作,并将结果放在字典中返回。位运算主要是通过NSFetchRequest对象的propertiesToFetch属性设置,这个属性可以设置多个描述对象,最后通过不同的name当做key来取出结果即可。
NSExpression类可以描述多种运算,可以在NSExpression.h文件中的注释部分,看到所有支持的运算类型,大概看了一下有二十多种运算。而且除了上面NSExpression调用的方法,此类还支持点语法的位运算,例如下面的例子。
[NSExpression expressionWithFormat:@"@sum.height"];
批处理
在使用CoreData之前,我和公司同事也讨论过,假设遇到需要大量数据处理的时候怎么办。CoreData对于大量数据处理的灵活性肯定不如SQLite,这时候还需要自己使用其他方式优化数据处理。虽然在移动端这种情况很少出现,但是在持久层设计时还是要考虑这方面。
当需要进行数据的处理时,CoreData需要先将数据加载到内存中,然后才能对数据进行处理。这样对于大量数据来说,都加载到内存中是非常消耗内存的,而且容易导致崩溃的发生。如果遇到更改所有数据的某个字段这样的简单需求,需要将相关的托管对象都加载到内存中,然后进行更改、保存。
对于上面这样的问题,CoreData在iOS8推出了批量更新API,通过这个API可以直接在数据库一层就完成更新操作,而不需要将数据加载到内存。除了批量更新操作,在iOS9中还推出了批量删除API,也是在数据库一层完成的操作。关于批处理的API很多都是iOS8、iOS9出来的,使用时需要注意版本兼容。
但是有个问题,批量更新和批量删除的两个API,都是直接对数据库进行操作,更新完之后会导致MOC缓存和本地持久化数据不同步的问题。所以需要手动刷新受影响的MOC中存储的托管对象,使MOC和本地统一。假设你使用了NSFetchedResultsController,为了保证界面和数据的统一,这一步更新操作更需要做。
批量更新
#pragma mark - ----- Batch Operation ------ /** 注意:无论是批量更新还是批量删除,这个批量操作都是发生在SQLite层的。然而在SQLite发生了批量操作后,并不会主动更新上层MOC中缓存的托管对象,所以在进行批量操作后,需要对相关的MOC进行更新操作。 虽然在客户端很少遇到大量数据处理的情况,但是如果遇到这样的需求,推荐使用批量处理API。 */ /** 批量更新 */ - (IBAction)batchUpdate:(UIButton *)sender { // 创建批量更新对象,并指明操作Student表 NSBatchUpdateRequest * updateRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:@"Student"]; // 设置返回值类型,默认是什么都不返回(NSStatusOnlyResultType),这里设置返回发生改变的对象Count值 updateRequest.resultType = NSUpdatedObjectsCountResultType; // 设置发生改变字段的字典 updateRequest.propertiesToUpdate = @{@"name" : @"lxz"}; // 执行请求后,返回值是一个特定的result对象,通过result的属性获取返回的结果。 // MOC的这个API是从iOS8出来的,所以需要注意版本兼容。 NSError * error = nil; NSBatchUpdateResult * result = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:updateRequest error:&error]; NSLog(@"batch update count is %ld", [result.result integerValue]); // 错误处理 if (error) { NSLog(@"batch update request result error : %@", error); } // 更新MOC中的托管对象,使MOC和本地持久化区数据同步 [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext refreshAllObjects]; }
上面对Employee表中所有的托管对象height值做了批量更新,在更新时通过设置propertiesToUpdate字典来控制更新字段和更新的值,设置格式是字段名 : 新值。通过设置批处理对象的predicate属性,设置一个谓词对象来控制受影响的对象。
还可以对多个存储区(数据库)做同样批处理操作,通过设置其父类的affectedStores属性,类型是一个数组,可以包含受影响的存储区,多个存储区的操作对批量删除同样适用。
MOC在执行请求方法时,发现方法名也不一样了,执行的是executeRequest: error:方法,这个方法是从iOS8之后出来的。方法传入的参数是NSBatchUpdateRequest类,此类并不是继承自NSFetchRequest类,而是直接继承自NSPersistentStoreRequest,和NSFetchRequest是平级关系。
批量删除
/** 批量删除 */ - (IBAction)batchDelete:(UIButton *)sender { // 创建请求对象,并指明对Student表做操作 NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 通过谓词设置过滤条件,设置条件为age小于20 NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < %ld", 20]; fetchRequest.predicate = predicate; // 创建批量删除请求,并使用上面创建的请求对象当做参数进行初始化 NSBatchDeleteRequest * deleteRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest]; // 设置请求结果类型,设置为受影响对象的Count deleteRequest.resultType = NSBatchDeleteResultTypeCount; // 使用NSBatchDeleteResult对象来接受返回结果,通过id类型的属性result获取结果 NSError * error = nil; NSBatchDeleteResult * result = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:deleteRequest error:&error]; NSLog(@"batch delete request result count is %ld", [result.result integerValue]); // 错误处理 if (error) { NSLog(@"batch delete request error : %@", error); } // 更新MOC中的托管对象,使MOC和本地持久化区数据同步 [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext refreshAllObjects]; }
大多数情况下,涉及到托管对象的操作,都需要将其加载到内存中完成。所以使用CoreData时,需要注意内存的使用,不要在内存中存在过多的托管对象。在已经做系统兼容的情况下,进行大量数据的操作时,应该尽量使用批处理来完成操作。
需要注意的是,refreshAllObjects是从iOS9出来的,在iOS9之前因为要做版本兼容,所以需要使用refreshObject: mergeChanges:方法更新托管对象。
异步请求
#pragma mark - ----- Asynchronous Request ------ /** 异步处理 */ - (IBAction)asyncRequest:(UIButton *)sender { // 创建请求对象,并指明操作Student表 NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"]; // 创建异步请求对象,并通过一个block进行回调,返回结果是一个NSAsynchronousFetchResult类型参数 NSAsynchronousFetchRequest * asycFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult * _Nonnull result) { // 通过返回结果的finalResult属性,获取结果数组 [result.finalResult enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { NSLog(@"fetch request result Student.count = %ld, Student.name = %@", result.finalResult.count, obj.name); }]; }]; // 执行异步请求,和批量处理执行同一个请求方法 NSError * error = nil; [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:asycFetchRequest error:&error]; // 错误处理 if (error) { NSLog(@"fetch request result error : %@", error); } }
上面通过NSAsynchronousFetchRequest对象创建了一个异步请求,并通过block进行回调。如果有多个请求同时发起,不需要担心线程安全的问题,系统会将所有的异步请求添加到一个操作队列中,在前一个任务访问数据库时,CoreData会将数据库加锁,等前面的执行完成才会继续执行后面的操作。
NSAsynchronousFetchRequest提供了cancel方法,也就是可以在请求过程中,将这个请求取消。还可以通过一个NSProgress类型的属性,获取请求完成进度。NSAsynchronousFetchRequest类从iOS8开始可以使用,所以低版本需要做版本兼容。
需要注意的是,执行请求时MOC并发类型不能是NSConfinementConcurrencyType,这个并发类型已经被抛弃,会导致崩溃。
4、CoreData-高级用法
NSFetchedResultsController
在开发过程中会经常用到UITableView这样的视图类,这些视图类需要自己管理其数据源,包括网络获取、本地存储都需要写代码进行管理。
而在CoreData中提供了NSFetchedResultsController类(fetched results controller,也叫FRC),FRC可以管理UITableView或UICollectionView的数据源。这个数据源主要指本地持久化的数据,也可以用这个数据源配合着网络请求数据一起使用,主要看业务需求了。
本篇文章会使用UITableView作为视图类,配合NSFetchedResultsController进行后面的演示,UICollectionView配合NSFetchedResultsController的使用也是类似,这里就不都讲了。
简单介绍
就像上面说到的,NSFetchedResultsController就像是上面两种视图的数据管理者一样。FRC可以监听一个MOC的改变,如果MOC执行了托管对象的增删改操作,就会对本地持久化数据发生改变,FRC就会回调对应的代理方法,回调方法的参数会包括执行操作的类型、操作的值、indexPath等参数。
实际使用时,通过FRC“绑定”一个MOC,将UITableView嵌入在FRC的执行流程中。在任何地方对这个“绑定”的MOC存储区做修改,都会触发FRC的回调方法,在FRC的回调方法中嵌入UITableView代码并做对应修改即可。
由此可以看出FRC最大优势就是,始终和本地持久化的数据保持统一。只要本地持久化的数据发生改变,就会触发FRC的回调方法,从而在回调方法中更新上层数据源和UI。这种方式讲的简单一点,就可以叫做数据带动UI。

但是需要注意一点,在FRC的初始化中传入了一个MOC参数,FRC只能监测传入的MOC发生的改变。假设其他MOC对同一个存储区发生了改变,FRC则不能监测到这个变化,不会做出任何反应。
所以使用FRC时,需要注意FRC只能对一个MOC的变化做出反应,所以在CoreData持久化层设计时,尽量一个存储区只对应一个MOC,或设置一个负责UI的MOC,这在后面多线程部分会详细讲解。
修改模型文件结构
在写代码之前,先对之前的模型文件结构做一些修改。

讲FRC的时候,只需要用到Employee这一张表,其他表和设置直接忽略。需要在Employee原有字段的基础上,增加一个String类型的sectionName字段,这个字段就是用来存储section title的,在下面的文章中将会详细讲到。
初始化FRC
下面例子是比较常用的FRC初始化方式,初始化时指定的MOC,还用之前讲过的MOC初始化代码,UITableView初始化代码这里也省略了,主要突出FRC的初始化。
#import "ChatViewController.h" #import "CoreDataManager.h" #import "User+CoreDataProperties.h" @interface ChatViewController ()<UITableViewDataSource, UITableViewDelegate,NSFetchedResultsControllerDelegate> @property (nonatomic, strong) CoreDataManager * manager; @property (nonatomic, strong) User * user; @property (nonatomic, strong) UITableView * tableView; @property (strong, nonatomic) NSFetchedResultsController * fetchedResultController; @end @implementation ChatViewController - (void)viewDidLoad { [super viewDidLoad]; _manager = [CoreDataManager sharedCoreDataManager]; _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, self.view.bounds.size.width, self.view.bounds.size.height-100) style:UITableViewStylePlain]; [self.view addSubview:_tableView]; _tableView.delegate = self; _tableView.dataSource = self; } #pragma mark - ----- UITableView Delegate ------ - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.fetchedResultController.sections.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.fetchedResultController.sections[section].numberOfObjects; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { _user = [self.fetchedResultController objectAtIndexPath:indexPath]; UITableViewCell * cell = [self.tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"identifier"]; } cell.textLabel.text = _user.username; cell.detailTextLabel.text = _user.age; return cell; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return self.fetchedResultController.sections[section].indexTitle; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return YES; } - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { if (editingStyle == UITableViewCellEditingStyleDelete) { // 删除托管对象 _user = [self.fetchedResultController objectAtIndexPath:indexPath]; [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext deleteObject:_user]; // 保存上下文环境,并做错误处理 NSError * error = nil; if (![[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error]) { NSLog(@"tableView delete cell error : %@", error); } } } #pragma mark - ----- NSFetchedResultsController ------ #pragma mark - ----- 生成测试数据 ------ //插入数据 - (IBAction)CreateTestData:(UIButton *)sender { for (int i = 0; i < 3; i++) { _user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext]; _user.username = [NSString stringWithFormat:@"username:%d", i]; _user.age = [NSString stringWithFormat:@"age:%d", i ]; _user.sectionName = [NSString stringWithFormat:@"sectionName:%d", i]; } NSError * error = nil; if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) { [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error]; } } - (IBAction)RefreshTestData:(UIButton *)sender { // 创建请求对象,并指明操作User表 NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"User"]; // 设置排序规则,指明根据age字段升序排序 NSSortDescriptor * ageSort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]; request.sortDescriptors = @[ageSort]; // 创建NSFetchedResultsController控制器实例,并绑定MOC NSError * error = nil; self.fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext sectionNameKeyPath:@"sectionName" cacheName:nil]; // 设置代理,并遵守协议 self.fetchedResultController.delegate = self; // 执行获取请求,执行后FRC会从持久化存储区加载数据,其他地方可以通过FRC获取数据 [self.fetchedResultController performFetch:&error]; // 错误处理 if (error) { NSLog(@"NSFetchedResultsController init error : %@", error); } // 刷新UI [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"identifier"]; [self.tableView reloadData]; } #pragma mark - ----- NSFetchedResultsControllerDelegate ------ // Cell数据源发生改变会回调此方法,例如添加新的托管对象等 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath { switch (type) { case NSFetchedResultsChangeInsert: [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: { User * user = [self.fetchedResultController objectAtIndexPath:indexPath]; UITableViewCell * cell = [self.tableView cellForRowAtIndexPath:indexPath]; cell.textLabel.text = user.username; [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; } break; } } // Section数据源发生改变回调此方法,例如修改section title等 - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch (type) { case NSFetchedResultsChangeInsert: [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } // 本地数据源发生改变,将要开始回调FRC代理方法。 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [self.tableView beginUpdates]; } // 本地数据源发生改变,FRC代理方法回调完成。 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [self.tableView endUpdates]; } - (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName { return [NSString stringWithFormat:@"sectionName %@", sectionName]; } - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; // Dispose of any resources that can be recreated. }
就像cellForRowAtIndexPath:方法中使用的一样,FRC提供了两个方法轻松转换indexPath和NSManagedObject的对象,在实际开发中这两个方法非常实用,这也是FRC和UITableView、UICollectionView深度融合的表现。
- (id)objectAtIndexPath:(NSIndexPath *)indexPath; - (nullable NSIndexPath *)indexPathForObject:(id)object;
Fetched Results Controller Delegate
// Cell数据源发生改变会回调此方法,例如添加新的托管对象等 - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeMove: [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeUpdate: { UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath]; Employee *emp = [fetchedResultController objectAtIndexPath:indexPath]; cell.textLabel.text = emp.name; } break; } } // Section数据源发生改变回调此方法,例如修改section title等。 - (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { switch (type) { case NSFetchedResultsChangeInsert: [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; case NSFetchedResultsChangeDelete: [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic]; break; default: break; } } // 本地数据源发生改变,将要开始回调FRC代理方法。 - (void)controllerWillChangeContent:(NSFetchedResultsController *)controller { [tableView beginUpdates]; } // 本地数据源发生改变,FRC代理方法回调完成。 - (void)controllerDidChangeContent:(NSFetchedResultsController *)controller { [tableView endUpdates]; } // 返回section的title,可以在这里对title做进一步处理。这里修改title后,对应section的indexTitle属性会被更新。 - (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName { return [NSString stringWithFormat:@"sectionName %@", sectionName]; }
上面就是当本地持久化数据发生改变后,被回调的FRC代理方法的实现,可以在对应的实现中完成自己的代码逻辑。
在上面的章节中讲到删除cell后,本地持久化数据同步的问题。在删除cell后在tableView代理方法的回调中,调用了MOC的删除方法,使本地持久化存储和UI保持同步,并回调到下面的FRC代理方法中,在代理方法中对UI做删除操作,这样一套由UI的改变引发的删除流程就完成了。
目前为止已经实现了数据和UI的双向同步,即UI发生改变后本地存储发生改变,本地存储发生改变后UI也随之改变。可以通过下面添加数据的代码来测试一下,NSFetchedResultsController就讲到这里了。
- (void)addMoreData { Employee *employee = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context]; employee.name = [NSString stringWithFormat:@"lxz 15"]; employee.height = @(15); employee.brithday = [NSDate date]; employee.sectionName = [NSString stringWithFormat:@"3"]; NSError *error = nil; if (![context save:&error]) { NSLog(@"MOC save error : %@", error); } }
版本迁移
CoreData版本迁移的方式有很多,一般都是先在Xcode中,原有模型文件的基础上,创建一个新版本的模型文件,然后在此基础上做不同方式的版本迁移。
本章节将会讲三种不同的版本迁移方案,但都不会讲太深,都是从使用的角度讲起,可以满足大多数版本迁移的需求。
为什么要版本迁移?
在已经运行程序并通过模型文件生成数据库后,再对模型文件进行的修改,如果只是修改已有实体属性的默认值、最大最小值、Fetch Request等属性自身包含的参数时,并不会发生错误。如果修改模型文件的结构,或修改属性名、实体名等,造成模型文件的结构发生改变,这样再次运行程序就会导致崩溃。
在开发测试过程中,可以直接将原有程序卸载就可以解决这个问题,但是本地之前存储的数据也会消失。如果是线上程序,就涉及到版本迁移的问题,否则会导致崩溃,并提示如下错误:
CoreData: error: Illegal attempt to save to a file that was never opened. "This NSPersistentStoreCoordinator has no persistent stores (unknown). It cannot perform a save operation.". No last error recorded.
然而在需求不断变化的过程中,后续版本肯定会对原有的模型文件进行修改,这时就需要用到版本迁移的技术,下面开始讲版本迁移的方案。
创建新版本模型文件
本文中讲的几种版本迁移方案,在迁移之前都需要对原有的模型文件创建新版本。
选中需要做迁移的模型文件 -> 点击菜单栏Editor -> Add Model Version -> 选择基于哪个版本的模型文件(一般都是选择目前最新的版本),新建模型文件完成。
对于新版本模型文件的命名,我在创建新版本模型文件时,一般会拿当前工程版本号当做后缀,这样在模型文件版本比较多的时候,就可以很容易将模型文件版本和工程版本对应起来。

添加完成后,会发现之前的模型文件会变成一个文件夹,里面包含着多个模型文件。

在新建的模型文件中,里面的文件结构和之前的文件结构相同。后续的修改都应该在新的模型文件上,之前的模型文件不要再动了,在修改完模型文件后,记得更新对应的模型类文件。
基于新的模型文件,对Employee实体做如下修改,下面的版本迁移也以此为例。

添加一个String类型的属性,设置属性名为sectionName。

此时还应该选中模型文件,设置当前模型文件的版本。这里选择将最新版本设置为刚才新建的1.1.0版本,模型文件设置工作完成。
Show The File Inspector -> Model Version -> Current 设置为最新版本。

对模型文件的设置已经完成了,接下来系统还要知道我们想要怎样迁移数据。在迁移过程中可能会存在多种可能,苹果将这个灵活性留给了我们完成。剩下要做的就是编写迁移方案以及细节的代码。
轻量级版本迁移
轻量级版本迁移方案非常简单,大多数迁移工作都是由系统完成的,只需要告诉系统迁移方式即可。在持久化存储协调器(PSC)初始化对应的持久化存储(NSPersistentStore)对象时,设置options参数即可,参数是一个字典。PSC会根据传入的字典,自动推断版本迁移的过程。
字典中设置的key:
NSMigratePersistentStoresAutomaticallyOption设置为YES,CoreData会试着把低版本的持久化存储区迁移到最新版本的模型文件。NSInferMappingModelAutomaticallyOption设置为YES,CoreData会试着以最为合理地方式自动推断出源模型文件的实体中,某个属性到底对应于目标模型文件实体中的哪一个属性。
版本迁移的设置是在创建MOC时给PSC设置的,为了使代码更直观,下面只给出发生变化部分的代码,其他MOC的初始化代码都不变。
// 设置版本迁移方案 NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES, NSInferMappingModelAutomaticallyOption : @YES}; // 创建持久化存储协调器,并将迁移方案的字典当做参数传入 [coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
修改实体名
假设需要对已存在实体进行改名操作,需要将重命名后的实体Renaming ID,设置为之前的实体名。下面是Employee实体进行操作。

修改后再使用实体时,应该将实体名设为最新的实体名,这里也就是Employee2,而且数据库中的数据也会迁移到Employee2表中。
Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.9; [context save:nil];
Mapping Model 迁移方案
轻量级迁移方案只是针对增加和改变实体、属性这样的一些简单操作,假设有更复杂的迁移需求,就应该使用Xcode提供的迁移模板(Mapping Model)。通过Xcode创建一个后缀为.xcmappingmodel的文件,这个文件是专门用来进行数据迁移用的,一些变化关系也会体现在模板中,看起来非常直观。
这里还以上面更改实体名,并迁移实体数据为例子,将Employee实体迁移到Employee2中。首先将Employee实体改名为Employee2,然后创建Mapping Model文件。
Command + N 新建文件 -> 选择 Mapping Model -> 选择源文件 Source Model -> 选择目标文件 Target Model -> 命名 Mapping Model 文件名 -> Create 创建完成。

现在就创建好一个Mapping Model文件,文件中显示了实体、属性、Relationships,源文件和目标文件之间的关系。实体命名是EntityToEntity的方式命名的,实体包含的属性和关联关系,都会被添加到迁移方案中(Entity Mapping,Attribute Mapping,Relationship Mapping)。
在迁移文件的下方是源文件和目标文件的关系。
![]()
在上面图中改名后的Employee2实体并没有迁移关系,由于是改名后的实体,系统还不知道实体应该怎样做迁移。所以选中Mapping Model文件的Employee2 Mappings,可以看到右侧边栏的Source为invalid value。因为要从Employee实体迁移数据过来,所以将其选择为Employee,迁移关系就设置完成了。
设置完成后,还应该将之前EmployeeToEmployee的Mappings删除,因为这个实体已经被Employee2替代,它的Mappings也被Employee2 Mappings所替代,否则会报错。

在实体的迁移过程中,还可以通过设置Predicate的方式,来简单的控制迁移过程。例如只需要迁移一部分指定的数据,就可以通过Predicate来指定。可以直接在右侧Filter Predicate的位置设置过滤条件,格式是$source.height < 100,$source代表数据源的实体。
更复杂的迁移需求
如果还存在更复杂的迁移需求,而且上面的迁移方式不能满足,可以考虑更复杂的迁移方式。假设要在迁移过程中,对迁移的数据进行更改,这时候上面的迁移方案就不能满足需求了。
对于上面提到的问题,在Mapping Model文件中选中实体,可以看到Custom Policy这个选项,选项对应的是NSEntityMigrationPolicy的子类,可以创建并设置一个子类,并重写这个类的方法来控制迁移过程。
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;
版本迁移总结
版本迁移在需求的变更中肯定是要发生的,但是我们应该尽量避免这样的情况发生。在最开始设计模型文件数据结构的时候,就应该设计一个比较完善并且容易应对变化的结构,这样后面就算发生变化也不会对结构主体造成大的改动。
5、CoreData-多线程
CoreData的多线程,其中会包括并发队列类型、线程安全等技术点。
MOC并发队列类型
在CoreData中MOC是支持多线程的,可以在创建MOC对象时,指定其并发队列的类型。当指定队列类型后,系统会将操作都放在指定的队列中执行,如果指定的是私有队列,系统会创建一个新的队列。但这都是系统内部的行为,我们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的。
NSManagedObjectContext并发队列类型:
- NSConfinementConcurrencyType : 如果使用
init方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。 - NSPrivateQueueConcurrencyType : 私有并发队列类型,操作都是在子线程中完成的。
- NSMainQueueConcurrencyType : 主并发队列类型,如果涉及到
UI相关的操作,应该考虑使用这个枚举值初始化上下文。
其中NSConfinementConcurrencyType类型在iOS9之后已经被苹果废弃,不建议使用这个API。使用此类型创建的MOC,调用某些比较新的CoreData的API可能会导致崩溃。
MOC多线程调用方式
在CoreData中MOC不是线程安全的,在多线程情况下使用MOC时,不能简单的将MOC从一个线程中传递到另一个线程中使用,这并不是CoreData的多线程,而且会出问题。对于MOC多线程的使用,苹果给出了自己的解决方案。
在创建的MOC中使用多线程,无论是私有队列还是主队列,都应该采用下面两种多线程的使用方式,而不是自己手动创建线程。调用下面方法后,系统内部会将任务派发到不同的队列中执行。可以在不同的线程中调用MOC的这两个方法,这个是允许的。
- (void)performBlock:(void (^)())block 异步执行的block,调用之后会立刻返回。 - (void)performBlockAndWait:(void (^)())block 同步执行的block,调用之后会等待这个任务完成,才会继续向下执行。
下面是多线程调用的示例代码,在多线程的环境下执行MOC的save方法,就是将save方法放在MOC的block体中异步执行,其他方法的调用也是一样的。
[context performBlock:^{
[context save:nil];
}];
但是需要注意的是,这两个block方法不能在NSConfinementConcurrencyType类型的MOC下调用,这个类型的MOC是不支持多线程的,只支持其他两种并发方式的MOC。
多线程的使用
在业务比较复杂的情况下,需要进行大量数据处理,并且还需要涉及到UI的操作。对于这种复杂需求,如果都放在主队列中,对性能和界面流畅度都会有很大的影响,导致用户体验非常差,降低屏幕FPS。对于这种情况,可以采取多个MOC配合的方式。
CoreData多线程的发展中,在iOS5经历了一次比较大的变化,之后可以更方便的使用多线程。从iOS5开始,支持设置MOC的parentContext属性,通过这个属性可以设置MOC的父MOC。下面会针对iOS5之前和之后,分别讲解CoreData的多线程使用。
尽管现在的开发中早就不兼容iOS5之前的系统了,但是作为了解这里还是要讲一下,而且这种同步方式在iOS5之后也是可以正常使用的,也有很多人还在使用这种同步方式,下面其他章节也是同理。
iOS5之前使用多个MOC
在iOS5之前实现MOC的多线程,可以创建多个MOC,多个MOC使用同一个PSC,并让多个MOC实现数据同步。通过这种方式不用担心PSC在调用过程中的线程问题,MOC在使用PSC进行save操作时,会对PSC进行加锁,等当前加锁的MOC执行完操作之后,其他MOC才能继续执行操作。
每一个PSC都对应着一个持久化存储区,PSC知道存储区中数据存储的数据结构,而MOC需要使用这个PSC进行save操作的实现。

这样做有一个问题,当一个MOC发生改变并持久化到本地时,系统并不会将其他MOC缓存在内存中的NSManagedObject对象改变。所以这就需要我们在MOC发生改变时,将其他MOC数据更新。
根据上面的解释,在下面例子中创建了一个主队列的mainMOC,主要用于UI操作。一个私有队列的backgroundMOC,用于除UI之外的耗时操作,两个MOC使用的同一个PSC。
简单情况下的数据同步
简单情况下的数据同步,是针对于只有一个MOC的数据发生改变,并提交存储区后,其他MOC更新时并没有对相同的数据做改变,只是单纯的同步数据的情况。
在NSManagedObjectContext类中,根据不同操作定义了一些通知。在一个MOC发生改变时,其他地方可以通过MOC中定义的通知名,来获取MOC发生的改变。在NSManagedObjectContext中定义了下面三个通知:
- NSManagedObjectContextWillSaveNotification
MOC将要向存储区存储数据时,调用这个通知。在这个通知中不能获取发生改变相关的NSManagedObject对象。 - NSManagedObjectContextDidSaveNotification
MOC向存储区存储数据后,调用这个通知。在这个通知中可以获取改变、添加、删除等信息,以及相关联的NSManagedObject对象。 - NSManagedObjectContextObjectsDidChangeNotification 在
MOC中任何一个托管对象发生改变时,调用这个通知。例如修改托管对象的属性。
通过监听NSManagedObjectContextDidSaveNotification通知,获取所有MOC的save操作。
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsContext:) name:NSManagedObjectContextDidSaveNotification object:nil];
不需要在通知的回调方法中,编写代码对比被修改的托管对象。MOC为我们提供了下面的方法,只需要将通知对象传入,系统会自动同步数据。
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification;
下面是通知中的实现代码,但是需要注意的是,由于通知是同步执行的,在通知对应的回调方法中所处的线程,和发出通知的MOC执行操作时所处的线程是同一个线程,也就是系统performBlock:回调方法分配的线程。
所以其他MOC在通知回调方法中,需要注意使用performBlock:方法,并在block体中执行操作。
- (void)settingsContext:(NSNotification *)noti { [context performBlock:^{ // 调用需要同步的MOC对象的merge方法,直接将通知对象当做参数传进去即可,系统会完成同步操作。 [context mergeChangesFromContextDidSaveNotification:noti]; }]; }
复杂情况下的数据同步
在一个MOC对本地存储区的数据发生改变,而其他MOC也对同样的数据做了改变,这样后面执行save操作的MOC就会冲突,并导致后面的save操作失败,这就是复杂情况下的数据合并。
这是因为每次一个MOC执行一次fetch操作后,会保存一个本地持久化存储的状态,当下次执行save操作时会对比这个状态和本地持久化状态是否一样。如果一样,则代表本地没有其他MOC对存储发生过改变;如果不一样,则代表本地持久化存储被其他MOC改变过,这就是造成冲突的根本原因。
对于这种冲突的情况,可以通过MOC对象指定解决冲突的方案,通过mergePolicy属性来设置方案。mergePolicy属性有下面几种可选的策略,默认是NSErrorMergePolicy方式,这也是唯一一个有NSError返回值的选项。
- NSErrorMergePolicy : 默认值,当出现合并冲突时,返回一个
NSError对象来描述错误,而MOC和持久化存储区不发生改变。 - NSMergeByPropertyStoreTrumpMergePolicy : 以本地存储为准,使用本地存储来覆盖冲突部分。
- NSMergeByPropertyObjectTrumpMergePolicy : 以
MOC的为准,使用MOC来覆盖本地存储的冲突部分。 - NSOverwriteMergePolicy : 以
MOC为准,用MOC的所有NSManagedObject对象覆盖本地存储的对应对象。 - NSRollbackMergePolicy : 以本地存储为准,
MOC所有的NSManagedObject对象被本地存储的对应对象所覆盖。
上面五种策略中,除了第一个NSErrorMergePolicy的策略,其他四种中NSMergeByPropertyStoreTrumpMergePolicy和NSRollbackMergePolicy,以及NSMergeByPropertyObjectTrumpMergePolicy和NSOverwriteMergePolicy看起来是重复的。
其实它们并不是冲突的,这四种策略的不同体现在,对没有发生冲突的部分应该怎么处理。NSMergeByPropertyStoreTrumpMergePolicy和NSMergeByPropertyObjectTrumpMergePolicy对没有冲突的部分,未冲突部分数据并不会受到影响。而NSRollbackMergePolicy和NSOverwriteMergePolicy则是无论是否冲突,直接全部替换。
题外话:
对于MOC的这种合并策略来看,有木有感觉到CoreData解决冲突的方式,和SVN解决冲突的方式特别像。。。
线程安全
无论是MOC还是托管对象,都不应该在其他MOC的线程中执行操作,这两个API都不是线程安全的。但MOC可以在其他MOC线程中调用performBlock:方法,切换到自己的线程执行操作。
如果其他MOC想要拿到托管对象,并在自己的队列中使用托管对象,这是不允许的,托管对象是不能直接传递到其他MOC的线程的。但是可以通过获取NSManagedObject的NSManagedObjectID对象,在其他MOC中通过NSManagedObjectID对象,从持久化存储区中获取NSManagedObject对象,这样就是允许的。NSManagedObjectID是线程安全,并且可以跨线程使用的。
可以通过MOC获取NSManagedObjectID对应的NSManagedObject对象,例如下面几个MOC的API。
NSManagedObject *object = [context objectRegisteredForID:objectID]; NSManagedObject *object = [context objectWithID:objectID];
通过NSManagedObject对象的objectID属性,获取NSManagedObjectID类型的objectID对象。
NSManagedObjectID *objectID = object.objectID;
CoreData多线程结构设计
上面章节中写的大多都是怎么用CoreData多线程,在掌握多线程的使用后,就可以根据公司业务需求,设计一套CoreData多线程结构了。对于多线程结构的设计,应该本着尽量减少主线程压力的角度去设计,将所有耗时操作都放在子线程中执行。
对于具体的设计我根据不同的业务需求,给出两种设计方案的建议。
两层设计方案
在项目中多线程操作比较简单时,可以创建一个主队列mainMOC,和一个或多个私有队列的backgroundMOC。将所有backgroundMOC的parentContext设置为mainMOC,采取这样的两层设计一般就能够满足大多数需求了。

将耗时操作都放在backgroundMOC中执行,mainMOC负责所有和UI相关的操作。所有和UI无关的工作都交给backgroundMOC,在backgroundMOC对数据发生改变后,调用save方法会将改变push到mainMOC中,再由mainMOC执行save方法将改变保存到存储区。
代码这里就不写了,和上面例子中设置parentContext代码一样,主要讲一下设计思路。
三层设计方案
但是我们发现,上面的save操作最后还是由mainMOC去执行的,backgroundMOC只是负责处理数据。虽然mainMOC只执行save操作并不会很耗时,但是如果save涉及的数据比较多,这样还是会对性能造成影响的。
虽然客户端很少涉及到大量数据处理的需求,但是假设有这样的需求。可以考虑在两层结构之上,给mainMOC之上再添加一个parentMOC,这个parentMOC也是私有队列的MOC,用于处理save操作。

这样CoreData存储的结构就是三层了,最底层是backgroundMOC负责处理数据,中间层是mainMOC负责UI相关操作,最上层也是一个backgroundMOC负责执行save操作。这样就将影响UI的所有耗时操作全都剥离到私有队列中执行,使性能达到了很好的优化。
需要注意的是,执行MOC相关操作时,不要阻塞当前主线程。所有MOC的操作应该是异步的,无论是子线程还是主线程,尽量少的使用同步block方法。
MOC同步时机
设置MOC的parentContext属性之后,parent对于child的改变是知道的,但是child对于parent的改变是不知道的。苹果这样设计,应该是为了更好的数据同步。
Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:backgroundMOC]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.7f; [backgroundMOC performBlock:^{ [backgroundMOC save:nil]; [mainMOC performBlock:^{ [mainMOC save:nil]; }]; }];
在上面这段代码中,mainMOC是backgroundMOC的parentContext。在backgroundMOC执行save方法前,backgroundMOC和mainMOC都不能获取到Employee的数据,在backgroundMOC执行完save方法后,自身上下文发生改变的同时,也将改变push到mainMOC中,mainMOC也具有了Employee对象。
所以在backgroundMOC的save方法执行时,是对内存中的上下文做了改变,当拥有PSC的mainMOC执行save方法后,是对本地存储区做了改变。
6、CoreData-MagicalRecord
CoreData是苹果自家推出的一个持久化框架,使用起来更加面向对象。但是在使用过程中会出现大量代码,而且CoreData学习曲线比较陡峭,如果掌握不好,在使用过程中很容易造成其他问题。
国外开发者开源了一个基于CoreData封装的第三方——MagicalRecord,就像是FMDB封装SQLite一样,MagicalRecord封装的CoreData,使得原生的CoreData更加容易使用。并且MagicalRecord降低了CoreData的使用门槛,不用去手动管理之前的PSC、MOC等对象。
根据Github上MagicalRecord的官方文档,MagicalRecord的优点主要有三条:
1. 清理项目中CoreData代码
2. 支持清晰、简单、一行式的查询操作
3. 当需要优化请求时,可以获取NSFetchRequest进行修改
添加MagicalRecord到项目中
将MagicalRecord添加到项目中,和使用其他第三方一样,可以通过下载源码和CocoaPods两种方式添加。
1. 从Github下载MagicalRecord源码,将源码直接拖到项目中,后续需要手动更新源码。
2. 也可以通过CocoaPods安装MagicalRecord,需要在Podfile中加入下面命令,后续只需要通过命令来更新。
pod "MagicalRecord"
在之前创建新项目时,通过勾选"Use Core Data"的方式添加CoreData到项目中,会在AppDelegate文件中生成大量CoreData相关代码。如果是大型项目,被占用的位置是很重要的。而对于MagicalRecord来说,只需要两行代码即可。
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 初始化CoreData堆栈,也可以指定初始化某个CoreData堆栈 [MagicalRecord setupCoreDataStack]; return YES; } - (void)applicationWillTerminate:(UIApplication *)application { // 在应用退出时,应该调用cleanUp方法 [MagicalRecord cleanUp]; }
MagicalRecord是支持CoreData的.xcdatamodeld文件的,使得CoreData这一优点可以继续使用。建立数据结构时还是像之前使用CoreData一样,通过.xcdatamodeld文件的方式建立。

支持iCloud
CoreData是支持iCloud的,MagicalRecord对iCloud相关的操作也做了封装,只需要使用MagicalRecord+iCloud.h类中提供的方法,就可以进行iCloud相关的操作。
例如下面是MagicalRecord+iCloud.h中的一个方法,需要将相关参数传入即可。
+ (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID localStoreNamed:(NSString *)localStore;
创建上下文
MagicalRecord对上下文的管理和创建也比较全面,下面是MagicalRecord提供的部分创建和获取上下文的代码。因为是给NSManagedObjectContext添加的Category,可以直接用NSManagedObjectContext类调用,使用非常方便。
但是需要注意,虽然系统帮我们管理了上下文对象,对于耗时操作仍然要放在后台线程中处理,并且在主线程中进行UI操作。
+ [NSManagedObjectContext MR_context] 设置默认的上下文为它的父级上下文,并发类型为NSPrivateQueueConcurrencyType + [NSManagedObjectContext MR_newMainQueueContext] 创建一个新的上下文,并发类型为NSMainQueueConcurrencyType + [NSManagedObjectContext MR_newPrivateQueueContext] 创建一个新的上下文,并发类型为NSPrivateQueueConcurrencyType + [NSManagedObjectContext MR_contextWithParent:] 创建一个新的上下文,允许自定义父级上下文,并发类型为NSPrivateQueueConcurrencyType + [NSManagedObjectContext MR_contextWithStoreCoordinator:] 创建一个新的上下文,并允许自定义持久化存储协调器,并发类型为NSPrivateQueueConcurrencyType + [NSManagedObjectContext MR_defaultContext] 获取默认上下文对象,项目中最基础的上下文对象,并发类型是NSMainQueueConcurrencyType
增删改查
MagicalRecord对NSManagedObject添加了一个Category,将增删改查等操作放在这个Category中,使得这些操作可以直接被NSManagedObject类及其子类调用。
.1. 增
对于托管模型的创建非常简单,不需要像之前还需要进行上下文的操作,现在这都是MagicalRecord帮我们完成的。
// 创建并插入到上下文中 Employee *emp = [Employee MR_createEntity];
.2. 删
// 从上下文中删除当前对象 [emp MR_deleteEntity];
.3. 改
// 获取一个上下文对象 NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext]; // 在当前上下文环境中创建一个新的Employee对象 Employee *emp = [Employee MR_createEntityInContext:defaultContext]; emp.name = @"lxz"; emp.brithday = [NSDate date]; emp.height = @1.7; // 保存修改到当前上下文中 [defaultContext MR_saveToPersistentStoreAndWait];
.4. 查
// 执行查找操作,并设置排序条件 NSArray *empSorted = [Employee MR_findAllSortedBy:@"height" ascending:YES];
自定义NSFetchRequest
下面示例代码中,Employee根据已有的employeeFilter谓词对象,创建了employeeRequest请求对象,并将请求对象做修改后,从MOC中获取请求结果,实现自定义查找条件。
NSPredicate *employeeFilter = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]; NSFetchRequest *employeeRequest = [Employee MR_requestAllWithPredicate:employeeFilter]; employeeRequest.fetchOffset = 10; employeeRequest.fetchLimit = 10; NSArray *employees = [Employee MR_executeFetchRequest:employeeRequest];
参数设置
.1. 可以通过修改MR_LOGGING_DISABLED预编译指令的值,控制log打印。
#define MR_LOGGING_DISABLED 1
.2.MagicalRecord在DEBUG模式下,对模型文件发生了更改,并且没有创建新的模型文件版本。MagicalRecord默认会将旧的持久化存储删除,创建新的持久化存储。
MagicalRecord中文文档
MagicalRecord的使用方法还有很多,这里只是将一些比较常用的拿出来讲讲,其他就不一一讲解了。在Github上有国人翻译的MagicalRecord官方文档,翻译的非常全面,而且是实时更新的。

浙公网安备 33010602011771号