网上关于CoreData的教程能搜到不少,但很多都是点到即止,真正实用的部分都没有讲到,而基本不需要的地方又讲了太多,所以我打算根据我的使用情况写这么一篇实用教程。内容将包括:创建entity、创建relation、插入、更新、删除、查询、条件查询、排序、分组等操作,并分享本人对CoreData的一些独立见解。当然,一个完整的代码也是必须有的。
声明:本文演示数据中所涉及到的人名皆是本文作者虚构,如有雷同,纯属巧合。
目录
- CoreData是不是ORM?
- 别的教程却讲了一大堆的东西
- 创建工程
- 逻辑及建模
- 要不要“三层架构”
- 写一个管理类
- 加一些实体并指定它们的关系
- 顺便谈谈约束
- 查询、排序、过滤和查询分页
- 打印SQL语句
- 使用预查询
- 修改及删除
- 返回非NSManagedObject的查询
- 获取ID
- NSFetchedResultController
- 完整代码及其它
一、CoreData是不是ORM?
在很多教程中,CoreData被认为是一套ORM框架,虽然它确实具备许多ORM的功能,但更准确地说,它其实是一套“可视化数据持久化框架”,通俗讲就是提供一个可视界面,帮助你把你的数据对象“持久化”到“磁盘”上,使得程序再次启动后它们都还在。关于CoreData是否ORM,和直接使用SQLite的关系,*上有一个被Closed的讨论,感兴趣自己看看:[Go to *]
二、别的教程却讲了一大堆的东西
CoreData的底层是用Sqlite3来实现的,当然你也可以换,但这样有什么好处呢?麻烦,且不知道有什么坑,即使你不换,坑也够多的了。我们需要了解的并不是它的每一个细节,而是我们要用到的部分,对于框架总体,只需要知道个大概就可以了。
我们在内存中的对象时如何最后写入Sqlite3数据库中去的?其实是通过一个叫“Coordinator”的东西,这个东西我们会在接下去的代码中会看到,它究竟是怎么实现的,就不要去关心了,反正之后我们也不会直接用到。另一个东西叫“Context”,我们所有的动作,都要执行在Context上,由这个Context去调用Coordinator。
其它呢?还有“Managed Object”,简称MO,我们要持久化的对象不能是自己随便创建的阿猫阿狗的类,必须是MO,通过CoreData查出来的对象也是MO(好吧,本文后面会讲到返回非MO的查询^_^),它们派生自NSManagedObject。
最后一个是MOM,就是“Managed Object Model”,看到Model我一开还搞糊涂,我以为是对象实体,其实它就是你创建的模型啊,在你的XCode的导航栏中看到的那个“xxx.xcdatamodeld”的玩意儿就是了,这根本没什么好说的。实在要说的话,我想说那个xxx.xcdatamodeld其实是一个目录,进去看里面有个叫xxx.xcdatamodel的文件,就是你的“建模”了,但最终生成到应用程序包(bundle)中的model以及sqlite3数据库文件的名字跟这个并不一致,后面我们能看到,这里先不表。
所以你真正要记住的东西无非就是:Context(上下文,所有动作都要执行在一个Context上)和MO。简单吧?
三、创建工程
我们来做一个小小的信息系统,用来管理大学校园中的老师、学生、班级和课程的关系。
创建一个Empty Application,叫“CollegeManagementSystem”,记得给“Use Core Data”打上勾。
“Use Core Data”这个勾给我们做了些额外的工作,一是将“CoreData.framework”增加到我们工程的Frameworks列表中来了。二是在AppDelegate中增加了一些关于CoreData的代码,前面提到的Coordinator,Context和MOM你都能看到:
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator *persistentStoreCoordinator;
在AppDelegate.m中还指定了底层所使用的那个Sqlite数据库文件的名字,记一下这个名字,之后我们要直接打开那个数据库文件看个究竟。如果你的工程没勾选“Use Core Data”这个选项,你也可以模仿一个新创建的“Use Core Data”的工程把必要的代码添加上去,完全没问题。
另外,这里有些东西要讲讲,在AppDelegate.m中:
// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"CollegeManagementSystem" withExtension:@"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
这段代码提及到的的“CollegeManagementSystem.momd”即是前面提到的MOM,这是编译器生成在应用程序bundle里的MOM的名称,和前面的XXX.xcdatamodeld是有些差别的。再看看同一文件中的“persistentStoreCoordinator”方法,里面会告诉你sqlite数据库文件的名称。
四、逻辑及建模
这里我先描述下我们的业务逻辑(跟现实可能有些出入,别在意这些细节):
- 一共有4个实体类型,教师,课程,学生和班级。
- 学生必须属于某个班级,删除一个班级,就会连带删除属于它的所有学生。
- 学生可以选修若干门课程,同一课程也可以被若干学生选修。
- 一门课程有且只有一个授课教师,而一个教师可以教多门课程,
- 删除一个教师,就会删除所有这个教师的授课课程。
- 一个班级有一个班主任,由一位教师担当,一位教师最多只能担当一个班级的班主任。
- 每个实体类型都有一个名字,学生实体还有一个年龄。
现在打开那个“CollegeManagementSystem.xcdatamodeld”来建模了,实体,也就是Entity,每个实体有若干个属性,也就是Attribute,如名字年龄,另有Relationship来描述实体之间的关系,如图去编辑吧(由于“class”跟Objective C的关键字冲突,所以我命名为“MyClass”):
编辑的过程应该不难,按照上图提示的地方去操作。各个Entity的name属性都是String类型,学生的Age为Interger型。
Relationship的关系:
序号 | 实体 | Relationship名称 | 目标 | 类型 | 删除规则 |
1 | MyClass | students | Student | To Many | Cascade |
2 | teacher | Teacher | To One | Nullify | |
3 | Course | teacher | Teacher | To One | Nullify |
4 | students | Student | To Many | Nullify | |
5 | Student | courses | Course | To Many | Nullify |
6 | myclass | MyClass | To One | Nullify | |
7 | Teacher | courses | Course | To Many | Cascade |
8 | myclass | MyClass | To One | Nullify |
以1号Relationship为例,MyClass这个Entity有一个叫students的Relationship,表示这个班级里有哪些学生,一个MyClass中有若干个Student,所以是To Many的关系,即一对多,删除班级后,对应的学生也要被删除,所以删除规则是Cascade。
另外还要给Relationship设置Inverse,即反向关系,若不指定会有warning。还是以序号1为例,一个MyClass包括哪些Student,反过来就是一个Student属于哪个MyClass,很明显,1号的Inverse就是6号。
弄好后将Editor Style设置为Graph,如下图:
这个自动生成的图还是蛮直观的,单箭头代表“一”,双箭头代表“多”,如Teacher和Course之间的关系就是“一Teacher对多个Course”的关系。
还有一步,就是生成MO的子类,新建文件,选择CoreData中的NSManagedObject subclass:
Next,选中,Next,全选中,Create,这四个Entity的subclass就生成了,它们派生自NSManagedObject。
五、要不要“三层架构”
“三层架构”恐怕是我们听得最多,用得最多,但到最后却往往因为要依循它而作茧自缚的东西,其实关于“三层架构”的理解我见过N个版本,其中见得最多的版本就是这三层:“UI层”,“业务逻辑层”和“数据访问层”。数据访问层直接访问数据库,负责对表的简单增删查改,如果业务逻辑就是对表的增删查改的话,那业务逻辑层基本什么都不用干,我想你能在网上找到的例子大多如此,更有一些代码生成工具,直接帮你根据你的表结构生成这“三层架构”,其实我认为这是“帮倒忙”,徒增一大堆垃圾代码。
根据我的实战经验,所谓三层,大多时候都只需要两层,即UI层和业务逻辑层,而数据访问层则归入业务逻辑层去,因为这两者密不可分,数据就是业务,业务就是数据。理论上来说,你将业务逻辑层和数据访问层分开,能做到在更换DBMS的时候,业务逻辑层不需要修改,但实际上这种事情百年不遇,更换DBMS绝对是伤筋动骨的事情,如果遇到,那基本上就是一切推倒重来了。
好,言归正传,我们在使用CoreData的时候到底需不需要分层,我认为不用,因为CoreData其实并不是一套ORM,前面说了,它是一套很直接了当的图形化的对象关系及持久化框架,对象直接呈现在你的界面上,存在于你的内存中,而对象是怎么存储在sqlite中的,你基本不用关心。如果把MO一定要归入数据访问层,其上层无法接触到的话,那么要增加不少代码,你得把MO转为你自己定义的OC对象,而且你这么一来,就没法方便地用到CoreData所提供的一些特性,如NSFetchedResultController,总而言之是很不方便。
如果前面讲的仅仅是“不方便”,那这点恐怕就是“大麻烦”,那就是你不得不维护一个ID,前面我们创建的这些实体,大家看有没有ID?没有对吧,因为CoreData会在内部帮我们创建好ID,一般情况下,我们根本不需要关心各个实体的ID是什么,因为我们都是直接获取实体并使用,没有说“帮我获取到ID为多少多少的实体”,如果你硬要把Managed Object们限制在数据访问层中,那么你要在你自定义的OC对象中放入一个ID,以此来创建跟Managed Object的对应关系,这不得不说是个大麻烦。如果你真打算这么干,那下文我也会提到如何获取到这个ID的方法。但我真的不推荐。
如果你需要的是比较复杂的业务逻辑,而不是简单的“持久化”,那么CoreData可能并不适合,这时候你可以根据自己的需求,去选择直接使用Sqlite或者别的方案了。
六、写一个管理类
虽然不需要分层,但我们还是需要这么一个管理类来让我们的代码更好看一些,我们尽量把CoreData的各种操作,放在这个管理类中,在我们这个小小的应用中,只需要这么一个单实例的管理类即可。
//CollegeManager.h @interface CollegeManager : NSObject
+ (CollegeManager*)sharedManager;
- (void)save;
- (void)deleteEntity:(NSManagedObject*)obj;
@end //CollegeManager.m #import "CollegeManager.h"
#import "AppDelegate.h" static CollegeManager* _sharedManager = nil; @implementation CollegeManager{
AppDelegate* appDelegate;
NSManagedObjectContext* appContext;
} + (CollegeManager*)sharedManager{
static dispatch_once_t once;
dispatch_once(&once, ^{
_sharedManager = [[self alloc] init];
});
return _sharedManager;
} - (id)init{
self = [super init];
appDelegate = [[UIApplication sharedApplication] delegate];
appContext = [appDelegate managedObjectContext];
return self;
} - (void)save{
[appDelegate saveContext];
} - (void)deleteEntity:(NSManagedObject*)obj{
[appContext deleteObject:obj];
[self save];
}
@end
目前自有一个save和一个deleteEntity方法,之后再根据需要一点点加。
七、加一些实体并指定它们的关系
准备工作做好了,我们要开始用了,如果要做一个带完整界面的demo,这需要大量的工作,估计讲界面创建的篇幅会远超CoreData,但本文的主题是CoreData,而不是如何做界面,所以还是直接拖几个button,执行几个动作,NSLog一些东西出来就行了。当然,在后面讲到NSFetchedResultContoller的时候,会有一个相对完整的界面。
好,我们往前面做的那个管理类中加一个方法,initData,即加数据,下面的代码都有很完整的注释,我想不需要太多解释了:
-(void)initData
{
//插入一些班级实体
//这个Mutable Array是为了方便后面建立实体关系使用(后面的也是)
NSMutableArray* arrMyClasses = [[NSMutableArray alloc] init];
NSArray* arrMyClassesName = @[@"99级1班",@"99级2班",@"99级3班"];
for (NSString* className in arrMyClassesName) {
MyClass* newMyClass = [NSEntityDescription insertNewObjectForEntityForName:@"MyClass" inManagedObjectContext:appContext];
newMyClass.name = className;
[arrMyClasses addObject:newMyClass];
} //插入一些学生实体
NSMutableArray *arrStudents = [[NSMutableArray alloc] init];
NSArray *studentInfo = @[
@{@"name":@"李斌", @"age":@},
@{@"name":@"*", @"age":@},
@{@"name":@"朱文", @"age":@},
@{@"name":@"李强", @"age":@},
@{@"name":@"高崇", @"age":@},
@{@"name":@"薛大", @"age":@},
@{@"name":@"裘千仞", @"age":@},
@{@"name":@"王波", @"age":@},
@{@"name":@"王鹏", @"age":@},
];
for (id info in studentInfo) {
NSString* name = [info objectForKey:@"name"];
NSNumber* age = [info objectForKey:@"age"];
Student* newStudent = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:appContext];
newStudent.name = name;
newStudent.age = age;
[arrStudents addObject:newStudent];
} //插入一些教师实体
NSMutableArray* arrTeachers = [[NSMutableArray alloc] init];
NSArray* arrTeachersName = @[@"王刚",@"谢力",@"徐开义",@"许宏权"];
for (NSString* teacherName in arrTeachersName) {
Teacher* newTeacher = [NSEntityDescription insertNewObjectForEntityForName:@"Teacher" inManagedObjectContext:appContext];
newTeacher.name = teacherName;
[arrTeachers addObject:newTeacher];
} //插入一些课程实体
NSMutableArray* arrCourses = [[NSMutableArray alloc] init];
NSArray* arrCoursesName = @[@"CAD",@"软件工程",@"线性代数",@"微积分",@"大学物理"];
for (NSString* courseName in arrCoursesName) {
Course* newCourse = [NSEntityDescription insertNewObjectForEntityForName:@"Course" inManagedObjectContext:appContext];
newCourse.name = courseName;
[arrCourses addObject:newCourse];
} //创建学生和班级的关系
//往班级1中加入几个学生(方法有多种)
MyClass* classOne = [arrMyClasses objectAtIndex:];
[classOne addStudentsObject:[arrStudents objectAtIndex:]];
[classOne addStudentsObject:[arrStudents objectAtIndex:]];
[[arrStudents objectAtIndex:] setMyclass:classOne]; //或者这样也可以
//往班级2中加入几个学生(用不同方法)
MyClass* classTwo = [arrMyClasses objectAtIndex:];
[classTwo addStudents:[NSSet setWithArray:[arrStudents subarrayWithRange:NSMakeRange(, )]]];
//往班级3中加入几个学生(再用不同的方法)
MyClass* classThree = [arrMyClasses objectAtIndex:];
[classThree setStudents:[NSSet setWithArray:[arrStudents subarrayWithRange:NSMakeRange(, )]]]; //给三个班指派班主任
Teacher* wanggang = [arrTeachers objectAtIndex:];
Teacher* xieli = [arrTeachers objectAtIndex:];
Teacher* xukaiyi = [arrTeachers objectAtIndex:];
Teacher* xuhongquan = [arrTeachers objectAtIndex:]; [classOne setTeacher:wanggang];
classTwo.teacher = xieli; //或这样(可能不太好)
[xukaiyi setMyclass: classThree]; //或这样反过来也行 //创建教师和课程的对应关系
Course* cad = [arrCourses objectAtIndex:];
Course* software = [arrCourses objectAtIndex:];
Course* linear = [arrCourses objectAtIndex:];
Course* calculus = [arrCourses objectAtIndex:];
Course* physics = [arrCourses objectAtIndex:];
[wanggang setCourses:[NSSet setWithObjects:cad, software, nil]];
[linear setTeacher:xieli];
[calculus setTeacher:xuhongquan];
[physics setTeacher:xukaiyi]; //设置学生所选修的课程
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:cad, software, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:cad, linear, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:linear, physics, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:physics, cad, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:calculus, physics, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:software, linear, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:software, physics, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:linear, software, nil]];
[[arrStudents objectAtIndex:] setCourses:[NSSet setWithObjects:calculus, software, cad, nil]]; //保存
//如不保存,上面的所有动作都不会写入sqlite
NSError* error;
[appContext save:&error];
if (error!=nil) {
NSLog(@"%@",error);
}
}
然后我们在界面上摆一个按钮,执行代码:
- (IBAction)onInitData:(id)sender {
[[CollegeManager sharedManager] initData];
}
如果一切顺利,什么提示都没有,OK,不要再点第二次了,否则又会再插入一堆数据,现在我们要做的事情就是看看到底生成了些什么。我们得找到那个sqlite数据库文件。在我的模拟器里,这个文件的位置在“/Users/guogangj/Library/Application Support/iPhone Simulator/7.1/Applications/D7B9C204-2617-4E95-98D7-D63D2700FE85/Documents”里(不难找),打开之:
$sqlite3 CollegeManagementSystem.sqlite
然后看看有哪些表:
sqlite> .schema
CREATE TABLE ZCOURSE ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZTEACHER INTEGER, ZNAME VARCHAR );
CREATE TABLE ZMYCLASS ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZTEACHER INTEGER, ZNAME VARCHAR );
CREATE TABLE ZSTUDENT ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZAGE INTEGER, ZMYCLASS INTEGER, ZNAME VARCHAR );
CREATE TABLE ZTEACHER ( Z_PK INTEGER PRIMARY KEY, Z_ENT INTEGER, Z_OPT INTEGER, ZMYCLASS INTEGER, ZNAME VARCHAR );
CREATE TABLE Z_1STUDENTS ( Z_1COURSES INTEGER, Z_3STUDENTS INTEGER, PRIMARY KEY (Z_1COURSES, Z_3STUDENTS) );
CREATE TABLE Z_METADATA (Z_VERSION INTEGER PRIMARY KEY, Z_UUID VARCHAR(), Z_PLIST BLOB);
CREATE TABLE Z_PRIMARYKEY (Z_ENT INTEGER PRIMARY KEY, Z_NAME VARCHAR, Z_SUPER INTEGER, Z_MAX INTEGER);
CREATE INDEX ZCOURSE_ZTEACHER_INDEX ON ZCOURSE (ZTEACHER);
CREATE INDEX ZMYCLASS_ZTEACHER_INDEX ON ZMYCLASS (ZTEACHER);
CREATE INDEX ZSTUDENT_ZMYCLASS_INDEX ON ZSTUDENT (ZMYCLASS);
CREATE INDEX ZTEACHER_ZMYCLASS_INDEX ON ZTEACHER (ZMYCLASS);
很明显,ZCOURSE,ZMYCLASS,ZSTUDENT和ZTEACHER就对应我们创建的那四个Entity,而Z_METADATA和Z_PRIMARYKEY分别是关于元数据和主键信息的表,跟我们没有直接关系,忽略之,那剩下的还有一张表,就是Z_1STUDENTS,这就是学生和课程的对应关系表,为什么别的Relationship都不需要一张独立的表,而这个需要?这是因为学生和课程之间的关系是多对多的关系,必须要一张额外的关系表来描述,CoreData很聪明,自动创建了这么一张表。我们来看看具体插入了什么数据:
sqlite> SELECT * FROM ZCOURSE;
||||CAD
||||线性代数
||||软件工程
||||微积分
||||大学物理
sqlite> SELECT * FROM ZMYCLASS;
||||99级3班
||||99级2班
||||99级1班
sqlite> SELECT * FROM ZSTUDENT;
|||||王波
|||||薛大
|||||朱文
|||||李斌
|||||*
|||||王鹏
|||||裘千仞
|||||李强
|||||高崇
sqlite> SELECT * FROM ZTEACHER;
||||王刚
||||徐开义
||||谢力
||||许宏权
sqlite> SELECT * FROM Z_1STUDENTS;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
虽然没有直接给出列名,但估计大家都清楚大致的含义了,细心的你也许还发现了,记录的顺序跟我们的插入顺序不一致,貌似这是乱的,这是因为我们是一起保存的,如果我们插入一条就保存一条,那么顺序就有保证了,但这个顺序其实意义不大,我们管它怎么保存?关键我们取的时候,按照我们的排序规则就可以了嘛。现在就来排一下序如何?
sqlite> select * from zstudent order by zname asc;
|||||朱文
|||||李强
|||||李斌
|||||*
|||||王波
|||||王鹏
|||||薛大
|||||裘千仞
|||||高崇
嗯?不对啊,为什么“朱”排在最前面了?这是因为这个排序是根据汉字的UNICODE编码进行的,并非我们所期待的拼音序。
八、顺便谈谈约束
约束是数据完整性的保障,DBMS通常会提供各种各样的约束,如非空约束、格式约束和外键约束等,但这些约束无疑带来了一些不方便的问题,以上面的数据为例,如果我做了一个约束,规定课程一定要有一个授课教师,那么我添加一个课程之前,我就必须先添加一个授课教师,我没办法做到各自添加了课程和授课教师后再指定它们之间的关系。
约束的另一个问题是对插入/删除的性能有少许影响,在数据量不大的时候,这点影响可以忽略不计,但数据量超大的时候,就逐渐逐渐有些感觉慢了,所以在大型互联网项目中,传统的这种实体约束关系设计就有些不流行了。
在我这个例子中,是没有使用什么约束条件的,用下来有没有问题大家可以看看,约束并非必须的东西。
OK,这纯粹是一点题外话……
九、查询、排序、过滤和查询分页
我们显然不能让用户使用命令行去查数据,现在我们来看看如何在程序中查询数据。先来一个简单点的:查出所有学生。
-(void)fetchTest
{
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Student" inManagedObjectContext:appContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription];
NSError *error = nil;
NSArray *arrStudents = [appContext executeFetchRequest:request error:&error];
if (error!=nil) {
NSLog(@"%@",error);
}
else{
for (Student* stu in arrStudents) {
NSLog(@"%@ (%@岁)",stu.name,stu.age);
}
}
}
查询的一般步骤是构造一个NSFetchRequest,一个NSFetchRequest中必须要指定一个NSEntityDescription,这是最基本的查询。现在我们稍微进一步,加上粗体字部分代码,按年龄对学生进行升序排序:
-(void)fetchTest
{
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Student" inManagedObjectContext:appContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription]; NSSortDescriptor* sorting = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sorting]]; NSError *error = nil;
NSArray *arrStudents = [appContext executeFetchRequest:request error:&error];
if (error!=nil) {
NSLog(@"%@",error);
}
else{
for (Student* stu in arrStudents) {
NSLog(@"%@ (%@岁)",stu.name,stu.age);
}
}
}
加一个过滤,查询出所有姓李的学生。
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name BEGINSWITH '李'"];
[request setPredicate:filter];
一样很简单,只需要加一个NSPredicate,NSPredicate并不是CoreData专有,它属于Foundation框架,通常用来表示一种过滤条件,官方文档见:[Go to developer.apple.com],这里还有一个不错的NSPredicate的教程:[Go to nshipster.com]。还有分页查询,即从第几条数据开始,最多取回第几条:
[request setFetchOffset:3];
[request setFetchLimit:3];
这样会从第4条记录开始,返回最多3条记录。我记得在SQL Server中,这种分页查询需要借助一个叫“ROW_NUMBER”的开窗函数来实现,比较麻烦,而这里则很简单。
前面那几个查询是不是太简单了?那我们能不能再来稍微复杂点的查询呢?好,现在我们要查询出选修了大学物理的学生。把查询条件稍微改改:
NSPredicate *filter = [NSPredicate predicateWithFormat:@"SUBQUERY(courses, $course, $course.name == '大学物理').@count > 0"];
[request setPredicate:filter];
这里用到了一个子查询,SUBQUERY,SUBQUERY的第一个参数是集合表达式,第二个是值表达式,第三个是条件,本例中的“@count”则表示集合函数count。NSPredicate的功能是十分强大的,我们在用到的时候再去搜索答案吧。
十、打印SQL语句
接下来,我想了解一下CoreData底层到底执行了哪些SQL语句,虽然实际中并不需要这样,但学习嘛,总归要知道怎么一回事。
XCode菜单,Product -> Scheme -> Edit Scheme,如图加入“-com.apple.CoreData.SQLDebug 1”。
现在,我们再执行一下以上的查询,就能在输出窗口看到这样的输出:
CollegeManagementSystem[1585:60b] CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZAGE, t0.ZNAME, t0.ZMYCLASS FROM ZSTUDENT t0 WHERE (SELECT COUNT(t2.Z_PK) FROM Z_1STUDENTS t1 JOIN ZCOURSE t2 ON t1.Z_1COURSES = t2.Z_PK WHERE (t0.Z_PK = t1.Z_3STUDENTS AND ( t2.ZNAME = ?)) ) > ?
十一、使用预查询
现在我们要查询所有班级,并逐个打印出班级的全体学生。
-(void)fetchMyClasses
{
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"MyClass" inManagedObjectContext:appContext];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entityDescription]; NSError *error = nil;
NSArray *arrClasses = [appContext executeFetchRequest:request error:&error];
if (error!=nil) {
NSLog(@"%@",error);
}
else{
for (MyClass* myclass in arrClasses) {
NSLog(@"%@",myclass.name);
for (Student* student in myclass.students) {
NSLog(@" %@", student.name);
}
}
}
}
代码执行没有问题,但,注意看一下输出,我的天啊,为什么执行了这么多SQL语句?
这是因为一开始查询MyClass的时候,并没有一起查询出Student,所以在遍历MyClass的students集合的时候,会逐个去查询Student,所以产生了大量的查询语句,这无疑是低效的,能一起查出来的东西为什么要分多次呢?OK,我们来改一下,其实很简单,只需要加这么一行:
[request setRelationshipKeyPathsForPrefetching:[NSArray arrayWithObjects:@"students",nil]];
再看看输出的日志,一切如你所愿。从中能看出CoreData其实有些坑,一不小心就掉进去了,不过你反观别的持久化或ORM工具,难道就没有坑么?
十二、修改及删除
修改和删除其实比前面提到的查询反而简单。
修改的方法:1,获取到要修改的Entity;2,修改其属性或关系;3,save。
删除的方法:1,获取到要删除的Entity;2,删除之;3,save。
看,首先都是要先获取Entity,所以不要用传统SQL的思想去要求它“帮我删除ID为XXX的记录”。首先看看Update的代码:
-(void)updateTest
{
//将“CAD”这门课的名称改为“CAD设计”,并将其授课教师改为“许宏权” //查出Teacher
//NSEntityDescription* entityDescription = [NSEntityDescription entityForName:@"Teacher" inManagedObjectContext:appContext];
//[request setEntity:entityDescription];
//前面这两步可以换成下面的一步
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Teacher"];
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name = '许宏权'"];
[request setPredicate:filter];
NSError *error = nil;
NSArray *arrResult = [appContext executeFetchRequest:request error:&error];
Teacher* xuhongquan = [arrResult objectAtIndex:]; //查出Course
request = [NSFetchRequest fetchRequestWithEntityName:@"Course"];
filter = [NSPredicate predicateWithFormat:@"name =[cd] 'cad'"]; //这里的[cd]表示大小写和音标不敏感
[request setPredicate:filter];
arrResult = [appContext executeFetchRequest:request error:&error];
Course* cad = [arrResult objectAtIndex:]; //修改
[cad setName:@"CAD设计"];
[cad setTeacher:xuhongquan]; //保存
[self save];
}
要修改,先查询,查询代码貌似有些繁,但实际上一般都是先查询好的,不会像现在这样显得头重脚轻,出错处理等这里也没做,这里仅仅是为了演示功能。下面是删除范例:
-(void)deleteTest
{
//删除学生“王波”
//查询出“王波”
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
NSPredicate *filter = [NSPredicate predicateWithFormat:@"name = '王波'"];
[request setPredicate:filter];
NSError *error = nil;
NSArray *arrResult = [appContext executeFetchRequest:request error:&error];
Student* wangbo = [arrResult objectAtIndex:];
//执行删除
[self deleteEntity:wangbo];
//保存
[self save]; //删除“99届2班”
request = [NSFetchRequest fetchRequestWithEntityName:@"MyClass"];
filter = [NSPredicate predicateWithFormat:@"name = '99级2班'"];
[request setPredicate:filter];
arrResult = [appContext executeFetchRequest:request error:&error];
MyClass* myClassTwo = [arrResult objectAtIndex:];
//执行删除
//注意!由于设置了删除规则为Cascade,所以“99届2班”的所有学生也会被同时删除掉
[self deleteEntity:myClassTwo];
//保存(其实也可以一起保存)
[self save]; //删除教师“徐开义”
request = [NSFetchRequest fetchRequestWithEntityName:@"Teacher"];
filter = [NSPredicate predicateWithFormat:@"name='徐开义'"];
[request setPredicate:filter];
arrResult = [appContext executeFetchRequest:request error:&error];
Teacher* teacher = [arrResult objectAtIndex:];
//执行删除
//注意!由于设置了删除规则为Cascade,所以“徐开义”的课程也会被删掉
[self deleteEntity:teacher];
//保存
[self save];
}
现在到命令行界面中看看删除的结果。看完后我们再初始化一下数据,后面还需要用到,到命令行界面中删除所有数据:
sqlite>DELETE FROM ZSTUDENT;
sqlite>DELETE FROM ZTEACHER;
sqlite>DELETE FROM ZMYCLASS;
sqlite>DELETE FROM ZCOURSE;
sqlite>DELETE FROM Z_1STUDENTS;
再执行一下initData即可。
十三、返回非NSManagedObject的查询
前面的查询返回的都是NSManagedObject的列表,但有时候我们要执行一些如sum,max,avg这样的统计,怎么办?还是以实际例子说明,先来一个最最简单的例子,查询课程总数。传统的SQL语句应该是:SELECT COUNT(1) FROM ZCOURSE;
-(void)countTest
{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Course"];
[request setResultType:NSCountResultType]; //关键是这步
NSError* error;
id result = [appContext executeFetchRequest:request error:&error];
NSLog(@"%@", [result objectAtIndex:]);
}
查出学生中最大的年龄。对应的SQL语句应该是:SELECT MAX(ZAGE) FROM ZSTUDENT;
-(void)maxTest
{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
[request setResultType:NSDictionaryResultType]; //必须设置为这个类型 //构造用于sum的ExpressionDescription(稍微有点繁琐啊)
NSExpression *theMaxExpression = [NSExpression expressionForFunction:@"max:" arguments:[NSArray arrayWithObject:[NSExpression expressionForKeyPath:@"age"]]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:@"maxAge"];
[expressionDescription setExpression:theMaxExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType]; //加入Request
[request setPropertiesToFetch:[NSArray arrayWithObjects:expressionDescription,nil]]; NSError* error;
id result = [appContext executeFetchRequest:request error:&error];
//返回的对象是一个字典的数组,取数组第一个元素,再用我们前面指定的key(也就是"maxAge")去获取我们想要的值
NSLog(@"The max age is : %@", [[result objectAtIndex:] objectForKey:@"maxAge"]);
}
查询出各种年龄段的学生数。对应的SQL语句是:SELECT ZAGE, COUNT(1) FROM ZSTUDENT GROUP BY ZAGE;
-(void)studentNumGroupByAge
{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
[request setResultType:NSDictionaryResultType]; //必须是这个 NSExpression *theCountExpression = [NSExpression expressionForFunction:@"count:" arguments:[NSArray arrayWithObject:[NSExpression expressionForKeyPath:@"name"]]];
NSExpressionDescription *expressionDescription = [[NSExpressionDescription alloc] init];
[expressionDescription setName:@"num"];
[expressionDescription setExpression:theCountExpression];
[expressionDescription setExpressionResultType:NSInteger32AttributeType]; //构造并加入Group By
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Student" inManagedObjectContext:appContext];
NSAttributeDescription* adultNumGroupBy = [entity.attributesByName objectForKey:@"age"];
[request setPropertiesToGroupBy:[NSArray arrayWithObject: adultNumGroupBy]]; [request setPropertiesToFetch:[NSArray arrayWithObjects:@"age",expressionDescription,nil]]; NSError* error;
id result = [appContext executeFetchRequest:request error:&error];
for (id item in result) {
NSLog(@"Age:%@ Student Num:%@", [item objectForKey:@"age"], [item objectForKey:@"num"]);
}
}
是不是觉得查询很繁琐?怎么把简简单单的SQL语句变得如此复杂?情况就是这样,我前面也提到了,CoreData其实并不适合处理复杂的业务逻辑,如果有那些复杂的业务逻辑的话,还是把它们放在服务器端好。
十四、获取ID
我是不推荐用ID,但你一定要用的话,可以这样获取到Entity的ID:
-(void)studentId{
NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
NSError* error;
id result = [appContext executeFetchRequest:request error:&error];
for (id stu in result) {
NSLog(@"%@", [stu objectID]); //objectID 返回的类型是 NSManagedObjectID
} //用ID获取MO的方法
NSManagedObjectID* firstStudentId = [[result objectAtIndex:] objectID];
Student* firstStudent = (Student*)[appContext existingObjectWithID:firstStudentId error:&error];
NSLog(@"First student name : %@", firstStudent.name);
}
看到了没?这个ID并不是一个简单地数字或者字符串,它的类型是NSManagedObjectID,如果你采用分层,那你是不是得让上层知道这么一个叫NSManagedObjectID的东西,不太合理啊,所以是不是考虑将这个ID进行转换?确实可以转,可以这样互转:
//将NSManagedObjectID转为NSURL
NSURL* urlFirstStudent = [firstStudentId URIRepresentation];
//将NSURL转为NSManagedObjectID
NSPersistentStoreCoordinator* coordinator = [appDelegate persistentStoreCoordinator];
NSManagedObjectID* firstStudentIdConvertBack = [coordinator managedObjectIDForURIRepresentation:urlFirstStudent];
NSLog(@"%@",firstStudentIdConvertBack);
呃……居然需要借助Coordinator,太麻烦了啊,够了!我想你肯定不想弄分层了,如果你还想,那看看下一节,相当方便的NSFetchedResultController,这个总归足够让你放弃分层了。
十五、NSFetchedResultController
我们前面所有的查询返回的结果都是NSArray类型的,则意味着都是“静态”的,如果sqlite里的数据发生了变化,我们是不知道的,而至于变化类型(变更,新增,删除,移动),那就更加不知道了。这些变更通知,NSFetchedResultController都有提供,(通过delegate)另外,NSFetchedResultController跟TableView结合得很好。
现在我们来做一个不完整的例子(但足够演示NSFetchedResultController了),那就是针对课程做一个TableView,可以新增,可以编辑,可以删除。看看效果图吧:
首先,我们的CollegeManager这次返回的是NSFetchedResultController,而不是NSArray了:
-(NSFetchedResultsController*) allCourses
{
NSFetchRequest *request = [[NSFetchRequest alloc] init]; //Entity
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"Course" inManagedObjectContext:appContext];
[request setEntity:entityDescription]; //Sort
//NSFetchedResultController必须有Sort
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"name" ascending:YES];
[request setSortDescriptors:[NSArray arrayWithObject:sort]]; NSFetchedResultsController* controller = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:appContext sectionNameKeyPath:nil cacheName:nil]; //Must perform fetch once.
NSError *error = nil;
[controller performFetch:&error]; return controller;
}
现在,界面的Controller需要实现NSFetchedResultsControllerDelegate:
@interface CourseViewController : UITableViewController<NSFetchedResultsControllerDelegate> @end
在界面的Controller中获取并保存这个NSFetchedResultController,并设置其delegate为self:
@interface CourseViewController ()
@property(nonatomic,strong) NSFetchedResultsController* fetchResultController;
@end //… - (void)viewDidLoad
{
[super viewDidLoad];
self.clearsSelectionOnViewWillAppear = YES; //Eidt button at the left navigation bar.
self.navigationItem.leftBarButtonItem = self.editButtonItem; //Set the FetchedResultController
NSFetchedResultsController* resultController = [[CollegeManager sharedManager] allCourses];
resultController.delegate = self;
self.fetchResultController = resultController;
}
然后就是对NSFetchedResultsControllerDelegate的实现:
#pragma mark NSFetchedResultsControllerDelegate
-(void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
NSLog(@"controllerWillChangeContent");
[self.tableView beginUpdates];
} -(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
NSLog(@"didChangeSection");
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break; case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
} } -(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
NSLog(@"didChangeObject");
UITableView *tableView = self.tableView; switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[tableView reloadRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]withRowAnimation:UITableViewRowAnimationFade];
break;
}
} -(void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(@"controllerDidChangeContent");
[self.tableView endUpdates];
}
至于编辑界面中的代码就略了,因为跟主题相关性不大。
十六、完整代码及其它
我不知道还要写些什么,个人觉得CoreData是不太好掌握的东西,还有很多内容本文没涉及到,大家在用的过程中一定会遇到别的问题,怎么办?当然是Google了,用英文Google,大多数时候,你都能很快找到你想要的答案。
最后,少不了完整代码的,我这次不直接提供zip包,而是把代码放到开源中国的git托管去,这样将来还可能更新一下。地址:http://git.oschina.net/guogangj/CollegeManagementSystem.git