网上关于CoreData的教程能搜到不少,但很多都是点到即止,真正实用的部分都没有讲到,而基本不需要的地方又讲了太多,所以我打算根据我的使用情况写这么一篇实用教程。内容将包括:创建entity、创建relation、插入、更新、删除、查询、条件查询、排序、分组等操作,并分享本人对CoreData的一些独立见解。当然,一个完整的代码也是必须有的。
声明:本文演示数据中所涉及到的人名皆是本文作者虚构,如有雷同,纯属巧合。
在很多教程中,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数据库文件的名称。
这里我先描述下我们的业务逻辑(跟现实可能有些出入,别在意这些细节):
现在打开那个“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":@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的列表,但有时候我们要执行一些如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,但你一定要用的话,可以这样获取到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,这个总归足够让你放弃分层了。
我们前面所有的查询返回的结果都是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,大多数时候,你都能很快找到你想要的答案。
代码:https://github.com/fgb031102/CoreData