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 = @(); // 通过上下文保存对象,并在保存前判断是否有更改
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"]
可以通过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 = ; // 设置分页,每次请求获取六个托管对象
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
对象的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", ];
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(, , 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
提供了两个方法轻松转换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 = @();
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 -> 选择基于哪个版本的模型文件(一般都是选择目前最新的版本),新建模型文件完成。
对于新版本模型文件的命名,我在创建新版本模型文件时,一般会拿当前工程版本号当做后缀,这样在模型文件版本比较多的时候,就可以很容易将模型文件版本和工程版本对应起来。
添加完成后,会发现之前的模型文件会变成一个文件夹,里面包含着多个模型文件。
在新建的模型文件中,里面的文件结构和之前的文件结构相同。后续的修改都应该在新的模型文件上,之前的模型文件不要再动了,在修改完模型文件后,记得更新对应的模型类文件。
基于新的模型文件,对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 = ;
employeeRequest.fetchLimit = ;
NSArray *employees = [Employee MR_executeFetchRequest:employeeRequest];
参数设置
.1. 可以通过修改MR_LOGGING_DISABLED
预编译指令的值,控制log
打印。
#define MR_LOGGING_DISABLED 1
.2.MagicalRecord
在DEBUG
模式下,对模型文件发生了更改,并且没有创建新的模型文件版本。MagicalRecord
默认会将旧的持久化存储删除,创建新的持久化存储。
MagicalRecord中文文档
MagicalRecord
的使用方法还有很多,这里只是将一些比较常用的拿出来讲讲,其他就不一一讲解了。在Github
上有国人翻译的MagicalRecord
官方文档,翻译的非常全面,而且是实时更新的。