1、初识CoreData

CoreData的结构构成:

coreData详解-LMLPHP

NSManagedObjectModel的构成:

coreData详解-LMLPHP

  可以通过Entity创建继承自NSManagedObject类的文件,这个文件就是开发中使用的托管对象,具备模型对象的表示功能,CoreData的本地持久化都是通过这个类及其子类完成的。

  在CoreData的整体结构中,主要分为两部分。一个是NSManagedObjectContext管理的模型部分,管理着所有CoreData的托管对象。一个是SQLite实现的本地持久化部分,负责和SQL数据库进行数据交互,主要由NSPersistentStore类操作。这就构成了CoreData的大体结构。

coreData详解-LMLPHP

  从图中可以看出,这两部分都是比较独立的,两部分的交互由一个持久化存储调度器(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对象。

coreData详解-LMLPHP

获取这个对象后,系统会默认将这个对象缓存到一个字典中,缓存之后也可以通过fetchedProperty字典获取fetchedProperty对象。

属性设置:

  • default Value: 设置默认值,除了二进制不能设置,其他类型几乎都能设置。

  • optional: 在使用时是否可选,也可以理解为如果设置为NO,只要向MOC进行save操作,这个属性是否必须有值。否则MOC进行操作时会失败并返回一个error,该选项默认为YES

  • transient: 设置当前属性是否只存在于内存,不被持久化到本地,如果设置为YES,这个属性就不参与持久化操作,属性的其他操作没有区别。transient非常适合存储一些在内存中缓存的数据,例如存储临时数据,这些数据每次都是不同的,而且不需要进行本地持久化,所以可以声明为transient的属性。

  • indexed: 设置当前属性是否是索引。添加索引后可以有效的提升检索操作的速度。但是对于删除这样的操作,删除索引后其他地方还需要做出相应的变化,所以速度会比较慢。

  • Validation: 通过Validation可以设置Max ValueMin Value,通过这两个条件来约定数据,对数据的存储进行一个验证。数值类型都有相同的约定方式,而字符串则是约定长度,date是约定时间。

  • Reg. Ex.(Regular Expression): 可以设置正则表达式,用来验证和控制数据,不对数据自身产生影响。(只能应用于String类型)

  • Allows External Storage: 当存储二进制文件时,如果遇到比较大的文件,是否存储在存储区之外。如果选择YES,存储文件大小超过1MB的文件,都会存储在存储区之外。否则大型文件存储在存储区内,会造成SQLite进行表操作时,效率受到影响。

Relationships设置:

  • delete rule: 定义关联属性的删除规则。在当前对象和其他对象有关联关系时,当前对象被删除后与之关联对象的反应。这个参数有四个枚举值,代码对应着模型文件的相同选项。

    NSNoActionDeleteRule 删除后没有任何操作,也不会将关联对象的关联属性指向nil。删除后使用关联对象的关联属性,可能会导致其他问题。

    NSNullifyDeleteRule 删除后会将关联对象的关联属性指向nil,这是默认值。

    NSCascadeDeleteRule 删除当前对象后,会将与之关联的对象也一并删除。

    NSDenyDeleteRule 在删除当前对象时,如果当前对象还指向其他关联对象,则当前对象不能被删除。

  • Type: 主要有两种类型,To OneTo Many,表示当前关系是一对多还是一对一。

实体:

  • Parent Entity: 可以在实体中创建继承关系,在一个实体的菜单栏中通过Parent Entity可以设置父实体,这样就存在了实体的继承关系,最后创建出来的托管模型类也是具有继承关系的。注意继承关系中属性名不要相同。

  使用了这样的继承关系后,系统会将子类继承父类的数据,存在父类的表中,所有继承自同一父类的子类都会将父类部分存放在父类的表中。这样可能会导致父类的表中数据量过多,造成性能问题。


2、CoreData-基础使用

Fetched Properties

  在实体最下面,有一个Fetched Properties选项,这个选项用的不多,这里就不细讲了。Fetched Properties用于定义查询操作,和NSFetchRequest功能相同。定义fetchedProperty对象后,可以通过NSManagedObjectModel类的fetchRequestFromTemplateWithName:substitutionVariables:方法或其他相关方法获取这个fetchedProperty对象。

coreData详解-LMLPHP

获取这个对象后,系统会默认将这个对象缓存到一个字典中,缓存之后也可以通过fetchedProperty字典获取fetchedProperty对象。

Fetch Requests

  在模型文件中Entities下面有一个Fetch Requests,这个也是配置请求对象的。但是这个使用起来更加直观,可以很容易的完成一些简单的请求配置。相对于上面讲到的Fetched Properties,这个还是更方便使用一些。

coreData详解-LMLPHP

  上面是对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 = @(); // 通过上下文保存对象,并在保存前判断是否有更改
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 = @();
}]; // 将上面的修改进行存储
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"]

  可以通过NSPredicateiOS中的集合对象执行过滤操作,可以是NSArrayNSSet及其子类。对不可变数组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,也就是设置更深层被匹配的目标。例如下面设置employeename属性为查找条件,就是用点语法设置的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表中namelxz的数据,并且将所有符合的数据用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 = ; // 设置分页,每次请求获取六个托管对象
request.fetchLimit = ; // 设置排序规则,这里设置年龄升序排序
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对象的resultTypeNSCountResultType,获取到结果的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文件中查看
*/
}

coreData详解-LMLPHP

从执行结果可以看到,MOC对所有查找到的托管对象height属性执行了求和操作,并将结果放在字典中返回。位运算主要是通过NSFetchRequest对象的propertiesToFetch属性设置,这个属性可以设置多个描述对象,最后通过不同的name当做key来取出结果即可。

NSExpression类可以描述多种运算,可以在NSExpression.h文件中的注释部分,看到所有支持的运算类型,大概看了一下有二十多种运算。而且除了上面NSExpression调用的方法,此类还支持点语法的位运算,例如下面的例子。

[NSExpression expressionWithFormat:@"@sum.height"];

批处理

  在使用CoreData之前,我和公司同事也讨论过,假设遇到需要大量数据处理的时候怎么办。CoreData对于大量数据处理的灵活性肯定不如SQLite,这时候还需要自己使用其他方式优化数据处理。虽然在移动端这种情况很少出现,但是在持久层设计时还是要考虑这方面。

  当需要进行数据的处理时,CoreData需要先将数据加载到内存中,然后才能对数据进行处理。这样对于大量数据来说,都加载到内存中是非常消耗内存的,而且容易导致崩溃的发生。如果遇到更改所有数据的某个字段这样的简单需求,需要将相关的托管对象都加载到内存中,然后进行更改、保存。

  对于上面这样的问题,CoreDataiOS8推出了批量更新API,通过这个API可以直接在数据库一层就完成更新操作,而不需要将数据加载到内存。除了批量更新操作,在iOS9中还推出了批量删除API,也是在数据库一层完成的操作。关于批处理的API很多都是iOS8iOS9出来的,使用时需要注意版本兼容

  但是有个问题,批量更新和批量删除的两个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", ];
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可以管理UITableViewUICollectionView的数据源。这个数据源主要指本地持久化的数据,也可以用这个数据源配合着网络请求数据一起使用,主要看业务需求了。

  本篇文章会使用UITableView作为视图类,配合NSFetchedResultsController进行后面的演示,UICollectionView配合NSFetchedResultsController的使用也是类似,这里就不都讲了。

简单介绍

  就像上面说到的,NSFetchedResultsController就像是上面两种视图的数据管理者一样。FRC可以监听一个MOC的改变,如果MOC执行了托管对象的增删改操作,就会对本地持久化数据发生改变,FRC就会回调对应的代理方法,回调方法的参数会包括执行操作的类型、操作的值、indexPath等参数。

  实际使用时,通过FRC“绑定”一个MOC,将UITableView嵌入在FRC的执行流程中。在任何地方对这个“绑定”MOC存储区做修改,都会触发FRC的回调方法,在FRC的回调方法中嵌入UITableView代码并做对应修改即可。

  由此可以看出FRC最大优势就是,始终和本地持久化的数据保持统一。只要本地持久化的数据发生改变,就会触发FRC的回调方法,从而在回调方法中更新上层数据源和UI。这种方式讲的简单一点,就可以叫做数据带动UI

coreData详解-LMLPHP

但是需要注意一点,在FRC的初始化中传入了一个MOC参数,FRC只能监测传入的MOC发生的改变。假设其他MOC对同一个存储区发生了改变,FRC则不能监测到这个变化,不会做出任何反应。

所以使用FRC时,需要注意FRC只能对一个MOC的变化做出反应,所以在CoreData持久化层设计时,尽量一个存储区只对应一个MOC,或设置一个负责UIMOC,这在后面多线程部分会详细讲解。

修改模型文件结构

在写代码之前,先对之前的模型文件结构做一些修改。

coreData详解-LMLPHP

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(, , self.view.bounds.size.width, self.view.bounds.size.height-) 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 = ; i < ; 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提供了两个方法轻松转换indexPathNSManagedObject的对象,在实际开发中这两个方法非常实用,这也是FRCUITableViewUICollectionView深度融合的表现。

- (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 = @();
employee.brithday = [NSDate date];
employee.sectionName = [NSString stringWithFormat:@""]; 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 -> 选择基于哪个版本的模型文件(一般都是选择目前最新的版本),新建模型文件完成。

对于新版本模型文件的命名,我在创建新版本模型文件时,一般会拿当前工程版本号当做后缀,这样在模型文件版本比较多的时候,就可以很容易将模型文件版本和工程版本对应起来

coreData详解-LMLPHP

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

coreData详解-LMLPHP

在新建的模型文件中,里面的文件结构和之前的文件结构相同。后续的修改都应该在新的模型文件上,之前的模型文件不要再动了,在修改完模型文件后,记得更新对应的模型类文件

基于新的模型文件,对Employee实体做如下修改,下面的版本迁移也以此为例。

coreData详解-LMLPHP

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

coreData详解-LMLPHP

此时还应该选中模型文件,设置当前模型文件的版本。这里选择将最新版本设置为刚才新建的1.1.0版本,模型文件设置工作完成。

Show The File Inspector -> Model Version -> Current 设置为最新版本。

coreData详解-LMLPHP

 

对模型文件的设置已经完成了,接下来系统还要知道我们想要怎样迁移数据。在迁移过程中可能会存在多种可能,苹果将这个灵活性留给了我们完成。剩下要做的就是编写迁移方案以及细节的代码。

轻量级版本迁移

轻量级版本迁移方案非常简单,大多数迁移工作都是由系统完成的,只需要告诉系统迁移方式即可。在持久化存储协调器(PSC)初始化对应的持久化存储(NSPersistentStore)对象时,设置options参数即可,参数是一个字典。PSC会根据传入的字典,自动推断版本迁移的过程。

字典中设置的key
  • NSMigratePersistentStoresAutomaticallyOption设置为YESCoreData会试着把低版本的持久化存储区迁移到最新版本的模型文件。
  • NSInferMappingModelAutomaticallyOption设置为YESCoreData会试着以最为合理地方式自动推断出源模型文件的实体中,某个属性到底对应于目标模型文件实体中的哪一个属性。

版本迁移的设置是在创建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实体进行操作。

coreData详解-LMLPHP

修改后再使用实体时,应该将实体名设为最新的实体名,这里也就是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 创建完成。

coreData详解-LMLPHP

  现在就创建好一个Mapping Model文件,文件中显示了实体、属性、Relationships,源文件和目标文件之间的关系。实体命名是EntityToEntity的方式命名的,实体包含的属性和关联关系,都会被添加到迁移方案中(Entity MappingAttribute MappingRelationship Mapping)。

在迁移文件的下方是源文件和目标文件的关系。

coreData详解-LMLPHP

  在上面图中改名后的Employee2实体并没有迁移关系,由于是改名后的实体,系统还不知道实体应该怎样做迁移。所以选中Mapping Model文件的Employee2 Mappings,可以看到右侧边栏的Sourceinvalid value。因为要从Employee实体迁移数据过来,所以将其选择为Employee,迁移关系就设置完成了。

  设置完成后,还应该将之前EmployeeToEmployeeMappings删除,因为这个实体已经被Employee2替代,它的Mappings也被Employee2 Mappings所替代,否则会报错。

coreData详解-LMLPHP

  在实体的迁移过程中,还可以通过设置Predicate的方式,来简单的控制迁移过程。例如只需要迁移一部分指定的数据,就可以通过Predicate来指定。可以直接在右侧Filter Predicate的位置设置过滤条件,格式是$source.height < 100$source代表数据源的实体。
coreData详解-LMLPHP

更复杂的迁移需求

  如果还存在更复杂的迁移需求,而且上面的迁移方式不能满足,可以考虑更复杂的迁移方式。假设要在迁移过程中,对迁移的数据进行更改,这时候上面的迁移方案就不能满足需求了。

  对于上面提到的问题,在Mapping Model文件中选中实体,可以看到Custom Policy这个选项,选项对应的是NSEntityMigrationPolicy的子类,可以创建并设置一个子类,并重写这个类的方法来控制迁移过程。

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;

版本迁移总结

版本迁移在需求的变更中肯定是要发生的,但是我们应该尽量避免这样的情况发生。在最开始设计模型文件数据结构的时候,就应该设计一个比较完善并且容易应对变化的结构,这样后面就算发生变化也不会对结构主体造成大的改动。

 

5、CoreData-多线程

CoreData的多线程,其中会包括并发队列类型、线程安全等技术点。

MOC并发队列类型

  在CoreDataMOC是支持多线程的,可以在创建MOC对象时,指定其并发队列的类型。当指定队列类型后,系统会将操作都放在指定的队列中执行,如果指定的是私有队列,系统会创建一个新的队列。但这都是系统内部的行为,我们并不能获取这个队列,队列由系统所拥有,并由系统将任务派发到这个队列中执行的

NSManagedObjectContext并发队列类型:
  • NSConfinementConcurrencyType : 如果使用init方法初始化上下文,默认就是这个并发类型。这个枚举值是不支持多线程的,从名字上也体现出来了。
  • NSPrivateQueueConcurrencyType : 私有并发队列类型,操作都是在子线程中完成的。
  • NSMainQueueConcurrencyType : 主并发队列类型,如果涉及到UI相关的操作,应该考虑使用这个枚举值初始化上下文。

其中NSConfinementConcurrencyType类型在iOS9之后已经被苹果废弃,不建议使用这个API。使用此类型创建的MOC,调用某些比较新的CoreDataAPI可能会导致崩溃。

MOC多线程调用方式

  在CoreDataMOC不是线程安全的,在多线程情况下使用MOC时,不能简单的将MOC从一个线程中传递到另一个线程中使用,这并不是CoreData的多线程,而且会出问题。对于MOC多线程的使用,苹果给出了自己的解决方案。

  在创建的MOC中使用多线程,无论是私有队列还是主队列,都应该采用下面两种多线程的使用方式,而不是自己手动创建线程。调用下面方法后,系统内部会将任务派发到不同的队列中执行。可以在不同的线程中调用MOC的这两个方法,这个是允许的。

- (void)performBlock:(void (^)())block            异步执行的block,调用之后会立刻返回。
- (void)performBlockAndWait:(void (^)())block 同步执行的block,调用之后会等待这个任务完成,才会继续向下执行。

下面是多线程调用的示例代码,在多线程的环境下执行MOCsave方法,就是将save方法放在MOCblock体中异步执行,其他方法的调用也是一样的。

[context performBlock:^{
[context save:nil];
}];

但是需要注意的是,这两个block方法不能在NSConfinementConcurrencyType类型的MOC下调用,这个类型的MOC是不支持多线程的,只支持其他两种并发方式的MOC

多线程的使用

在业务比较复杂的情况下,需要进行大量数据处理,并且还需要涉及到UI的操作。对于这种复杂需求,如果都放在主队列中,对性能和界面流畅度都会有很大的影响,导致用户体验非常差,降低屏幕FPS。对于这种情况,可以采取多个MOC配合的方式。

CoreData多线程的发展中,在iOS5经历了一次比较大的变化,之后可以更方便的使用多线程。从iOS5开始,支持设置MOCparentContext属性,通过这个属性可以设置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操作的实现。

coreData详解-LMLPHP

  这样做有一个问题,当一个MOC发生改变并持久化到本地时,系统并不会将其他MOC缓存在内存中的NSManagedObject对象改变。所以这就需要我们在MOC发生改变时,将其他MOC数据更新。

  根据上面的解释,在下面例子中创建了一个主队列的mainMOC,主要用于UI操作。一个私有队列的backgroundMOC,用于除UI之外的耗时操作,两个MOC使用的同一个PSC

简单情况下的数据同步

简单情况下的数据同步,是针对于只有一个MOC的数据发生改变,并提交存储区后,其他MOC更新时并没有对相同的数据做改变,只是单纯的同步数据的情况。

NSManagedObjectContext类中,根据不同操作定义了一些通知。在一个MOC发生改变时,其他地方可以通过MOC中定义的通知名,来获取MOC发生的改变。在NSManagedObjectContext中定义了下面三个通知:

  • NSManagedObjectContextWillSaveNotification MOC将要向存储区存储数据时,调用这个通知。在这个通知中不能获取发生改变相关的NSManagedObject对象。
  • NSManagedObjectContextDidSaveNotification MOC向存储区存储数据后,调用这个通知。在这个通知中可以获取改变、添加、删除等信息,以及相关联的NSManagedObject对象。
  • NSManagedObjectContextObjectsDidChangeNotificationMOC中任何一个托管对象发生改变时,调用这个通知。例如修改托管对象的属性。

通过监听NSManagedObjectContextDidSaveNotification通知,获取所有MOCsave操作。

[[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的策略,其他四种中NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,以及NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy看起来是重复的。

其实它们并不是冲突的,这四种策略的不同体现在,对没有发生冲突的部分应该怎么处理NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy对没有冲突的部分,未冲突部分数据并不会受到影响。而NSRollbackMergePolicyNSOverwriteMergePolicy则是无论是否冲突,直接全部替换。

题外话:
对于MOC的这种合并策略来看,有木有感觉到CoreData解决冲突的方式,和SVN解决冲突的方式特别像。。。

线程安全

无论是MOC还是托管对象,都不应该在其他MOC的线程中执行操作,这两个API都不是线程安全的。但MOC可以在其他MOC线程中调用performBlock:方法,切换到自己的线程执行操作。

如果其他MOC想要拿到托管对象,并在自己的队列中使用托管对象,这是不允许的,托管对象是不能直接传递到其他MOC的线程的。但是可以通过获取NSManagedObjectNSManagedObjectID对象,在其他MOC中通过NSManagedObjectID对象,从持久化存储区中获取NSManagedObject对象,这样就是允许的。NSManagedObjectID是线程安全,并且可以跨线程使用的。

可以通过MOC获取NSManagedObjectID对应的NSManagedObject对象,例如下面几个MOCAPI

NSManagedObject *object = [context objectRegisteredForID:objectID];
NSManagedObject *object = [context objectWithID:objectID];

通过NSManagedObject对象的objectID属性,获取NSManagedObjectID类型的objectID对象。

NSManagedObjectID *objectID = object.objectID;

CoreData多线程结构设计

上面章节中写的大多都是怎么用CoreData多线程,在掌握多线程的使用后,就可以根据公司业务需求,设计一套CoreData多线程结构了。对于多线程结构的设计,应该本着尽量减少主线程压力的角度去设计,将所有耗时操作都放在子线程中执行。

对于具体的设计我根据不同的业务需求,给出两种设计方案的建议。

两层设计方案

在项目中多线程操作比较简单时,可以创建一个主队列mainMOC,和一个或多个私有队列的backgroundMOC。将所有backgroundMOCparentContext设置为mainMOC,采取这样的两层设计一般就能够满足大多数需求了。

coreData详解-LMLPHP

将耗时操作都放在backgroundMOC中执行,mainMOC负责所有和UI相关的操作。所有和UI无关的工作都交给backgroundMOC,在backgroundMOC对数据发生改变后,调用save方法会将改变pushmainMOC中,再由mainMOC执行save方法将改变保存到存储区。

代码这里就不写了,和上面例子中设置parentContext代码一样,主要讲一下设计思路。

三层设计方案

但是我们发现,上面的save操作最后还是由mainMOC去执行的,backgroundMOC只是负责处理数据。虽然mainMOC只执行save操作并不会很耗时,但是如果save涉及的数据比较多,这样还是会对性能造成影响的。

虽然客户端很少涉及到大量数据处理的需求,但是假设有这样的需求。可以考虑在两层结构之上,给mainMOC之上再添加一个parentMOC,这个parentMOC也是私有队列的MOC,用于处理save操作。

coreData详解-LMLPHP

这样CoreData存储的结构就是三层了,最底层是backgroundMOC负责处理数据,中间层是mainMOC负责UI相关操作,最上层也是一个backgroundMOC负责执行save操作。这样就将影响UI的所有耗时操作全都剥离到私有队列中执行,使性能达到了很好的优化。

需要注意的是,执行MOC相关操作时,不要阻塞当前主线程。所有MOC的操作应该是异步的,无论是子线程还是主线程,尽量少的使用同步block方法。

MOC同步时机

设置MOCparentContext属性之后,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];
}];
}];

在上面这段代码中,mainMOCbackgroundMOCparentContext。在backgroundMOC执行save方法前,backgroundMOCmainMOC都不能获取到Employee的数据,在backgroundMOC执行完save方法后,自身上下文发生改变的同时,也将改变pushmainMOC中,mainMOC也具有了Employee对象。

所以在backgroundMOCsave方法执行时,是对内存中的上下文做了改变,当拥有PSCmainMOC执行save方法后,是对本地存储区做了改变。

6、CoreData-MagicalRecord

CoreData是苹果自家推出的一个持久化框架,使用起来更加面向对象。但是在使用过程中会出现大量代码,而且CoreData学习曲线比较陡峭,如果掌握不好,在使用过程中很容易造成其他问题。

国外开发者开源了一个基于CoreData封装的第三方——MagicalRecord,就像是FMDB封装SQLite一样,MagicalRecord封装的CoreData,使得原生的CoreData更加容易使用。并且MagicalRecord降低了CoreData的使用门槛,不用去手动管理之前的PSCMOC等对象。

根据GithubMagicalRecord的官方文档,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文件的方式建立。

coreData详解-LMLPHP

支持iCloud

CoreData是支持iCloud的,MagicalRecordiCloud相关的操作也做了封装,只需要使用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

增删改查

MagicalRecordNSManagedObject添加了一个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 = ;
employeeRequest.fetchLimit = ;
NSArray *employees = [Employee MR_executeFetchRequest:employeeRequest];

参数设置

.1. 可以通过修改MR_LOGGING_DISABLED预编译指令的值,控制log打印。

#define MR_LOGGING_DISABLED 1

.2.MagicalRecordDEBUG模式下,对模型文件发生了更改,并且没有创建新的模型文件版本。MagicalRecord默认会将旧的持久化存储删除,创建新的持久化存储。

MagicalRecord中文文档

MagicalRecord的使用方法还有很多,这里只是将一些比较常用的拿出来讲讲,其他就不一一讲解了。在Github上有国人翻译的MagicalRecord官方文档,翻译的非常全面,而且是实时更新的。

MagicalRecord中文文档

原文链接:https://github.com/DeveloperErenLiu/BlogDemo-CoreData
05-11 03:41