一、CoreData是不是ORM?
在很多教程中,CoreData被认为是一套ORM框架,虽然它确实具备许多ORM的功能,但更准确地说,它其实是一套“可视化数据持久化框架”,通俗讲就是提供一个可视界面,帮助你把你的数据对象“持久化”到“磁盘”上,使得程序再次启动后它们都还在。关于CoreData是否ORM,和直接使用SQLite的关系,StackOverflow上有一个被Closed的讨论,感兴趣自己看看:[Go to StackOverflow]
二、别的教程却讲了一大堆的东西
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的关系:
以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":@20},
@{@"name":@"李鹏", @"age":@19},
@{@"name":@"朱文", @"age":@21},
@{@"name":@"李强", @"age":@21},
@{@"name":@"高崇", @"age":@18},
@{@"name":@"薛大", @"age":@19},
@{@"name":@"裘千仞", @"age":@21},
@{@"name":@"王波", @"age":@18},
@{@"name":@"王鹏", @"age":@19},
];
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:0];
[classOne addStudentsObject:[arrStudents objectAtIndex:0]];
[classOne addStudentsObject:[arrStudents objectAtIndex:1]];
[[arrStudents objectAtIndex:2] setMyclass:classOne]; //或者这样也可以
//往班级2中加入几个学生(用不同方法)
MyClass* classTwo = [arrMyClasses objectAtIndex:1];
[classTwo addStudents:[NSSet setWithArray:[arrStudents subarrayWithRange:NSMakeRange(3, 3)]]];
//往班级3中加入几个学生(再用不同的方法)
MyClass* classThree = [arrMyClasses objectAtIndex:2];
[classThree setStudents:[NSSet setWithArray:[arrStudents subarrayWithRange:NSMakeRange(6, 3)]]];
//给三个班指派班主任
Teacher* wanggang = [arrTeachers objectAtIndex:0];
Teacher* xieli = [arrTeachers objectAtIndex:1];
Teacher* xukaiyi = [arrTeachers objectAtIndex:2];
Teacher* xuhongquan = [arrTeachers objectAtIndex:3];
[classOne setTeacher:wanggang];
classTwo.teacher = xieli; //或这样(可能不太好)
[xukaiyi setMyclass: classThree]; //或这样反过来也行
//创建教师和课程的对应关系
Course* cad = [arrCourses objectAtIndex:0];
Course* software = [arrCourses objectAtIndex:1];
Course* linear = [arrCourses objectAtIndex:2];
Course* calculus = [arrCourses objectAtIndex:3];
Course* physics = [arrCourses objectAtIndex:4];
[wanggang setCourses:[NSSet setWithObjects:cad, software, nil]];
[linear setTeacher:xieli];
[calculus setTeacher:xuhongquan];
[physics setTeacher:xukaiyi];
//设置学生所选修的课程
[[arrStudents objectAtIndex:0] setCourses:[NSSet setWithObjects:cad, software, nil]];
[[arrStudents objectAtIndex:1] setCourses:[NSSet setWithObjects:cad, linear, nil]];
[[arrStudents objectAtIndex:2] setCourses:[NSSet setWithObjects:linear, physics, nil]];
[[arrStudents objectAtIndex:3] setCourses:[NSSet setWithObjects:physics, cad, nil]];
[[arrStudents objectAtIndex:4] setCourses:[NSSet setWithObjects:calculus, physics, nil]];
[[arrStudents objectAtIndex:5] setCourses:[NSSet setWithObjects:software, linear, nil]];
[[arrStudents objectAtIndex:6] setCourses:[NSSet setWithObjects:software, physics, nil]];
[[arrStudents objectAtIndex:7] setCourses:[NSSet setWithObjects:linear, software, nil]];
[[arrStudents objectAtIndex:8] 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(255), 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;
1|1|1|1|CAD
2|1|1|3|线性代数
3|1|1|1|软件工程
4|1|1|4|微积分
5|1|1|2|大学物理
sqlite> SELECT * FROM ZMYCLASS;
1|2|1|2|99级3班
2|2|1|3|99级2班
3|2|1|1|99级1班
sqlite> SELECT * FROM ZSTUDENT;
1|3|1|18|1|王波
2|3|1|19|2|薛大
3|3|1|21|3|朱文
4|3|1|20|3|李斌
5|3|1|19|3|李鹏
6|3|1|19|1|王鹏
7|3|1|21|1|裘千仞
8|3|1|21|2|李强
9|3|1|18|2|高崇
sqlite> SELECT * FROM ZTEACHER;
1|4|1|3|王刚
2|4|1|1|徐开义
3|4|1|2|谢力
4|4|1||许宏权
sqlite> SELECT * FROM Z_1STUDENTS;
3|4
3|1
3|6
3|2
3|7
2|5
2|3
2|2
2|1
5|7
5|8
5|3
5|9
4|9
4|6
1|5
1|8
1|4
1|6
虽然没有直接给出列名,但估计大家都清楚大致的含义了,细心的你也许还发现了,记录的顺序跟我们的插入顺序不一致,貌似这是乱的,这是因为我们是一起保存的,如果我们插入一条就保存一条,那么顺序就有保证了,但这个顺序其实意义不大,我们管它怎么保存?关键我们取的时候,按照我们的排序规则就可以了嘛。现在就来排一下序如何?
sqlite> select * from zstudent order by zname asc;
3|3|1|21|3|朱文
8|3|1|21|2|李强
4|3|1|20|3|李斌
5|3|1|19|3|李鹏
1|3|1|18|1|王波
6|3|1|19|1|王鹏
2|3|1|19|2|薛大
7|3|1|21|1|裘千仞
9|3|1|18|2|高崇
嗯?不对啊,为什么“朱”排在最前面了?这是因为这个排序是根据汉字的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:0];
//查出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:0];
//修改
[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:0];
//执行删除
[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:0];
//执行删除
//注意!由于设置了删除规则为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:0];
//执行删除
//注意!由于设置了删除规则为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:0]);
}
查出学生中最大的年龄。对应的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:0] 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:0] 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];
}
另一个不错的http://www.cnblogs.com/xiaodao/archive/2012/10/08/2715477.html博客。