CoreData使用详解

网上关于CoreData的教程能搜到不少,但很多都是点到即止,真正实用的部分都没有讲到,而基本不需要的地方又讲了太多,所以我打算根据我的使用情况写这么一篇实用教程。内容将包括:创建entity、创建relation、插入、更新、删除、查询、条件查询、排序、分组等操作,并分享本人对CoreData的一些独立见解。当然,一个完整的代码也是必须有的。

声明:本文演示数据中所涉及到的人名皆是本文作者虚构,如有雷同,纯属巧合。

目录

  1. CoreData是不是ORM?
  2. 别的教程却讲了一大堆的东西
  3. 创建工程
  4. 逻辑及建模
  5. 要不要“三层架构”
  6. 写一个管理类
  7. 加一些实体并指定它们的关系
  8. 顺便谈谈约束
  9. 查询、排序、过滤和查询分页
  10. 打印SQL语句
  11. 使用预查询
  12. 修改及删除
  13. 返回非NSManagedObject的查询
  14. 获取ID
  15. NSFetchedResultController
  16. 完整代码及其它

一、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”打上勾。

CoreData使用详解_第1张图片

“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数据库文件的名称。

四、逻辑及建模

这里我先描述下我们的业务逻辑(跟现实可能有些出入,别在意这些细节):

  1. 一共有4个实体类型,教师,课程,学生和班级。
  2. 学生必须属于某个班级,删除一个班级,就会连带删除属于它的所有学生。
  3. 学生可以选修若干门课程,同一课程也可以被若干学生选修。
  4. 一门课程有且只有一个授课教师,而一个教师可以教多门课程,
  5. 删除一个教师,就会删除所有这个教师的授课课程。
  6. 一个班级有一个班主任,由一位教师担当,一位教师最多只能担当一个班级的班主任。
  7. 每个实体类型都有一个名字,学生实体还有一个年龄。

现在打开那个“CollegeManagementSystem.xcdatamodeld”来建模了,实体,也就是Entity,每个实体有若干个属性,也就是Attribute,如名字年龄,另有Relationship来描述实体之间的关系,如图去编辑吧(由于“class”跟Objective C的关键字冲突,所以我命名为“MyClass”):

CoreData使用详解_第2张图片

编辑的过程应该不难,按照上图提示的地方去操作。各个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:

CoreData使用详解_第3张图片

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”。

CoreData使用详解_第4张图片

现在,我们再执行一下以上的查询,就能在输出窗口看到这样的输出:

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语句?

CoreData使用详解_第5张图片

这是因为一开始查询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,可以新增,可以编辑,可以删除。看看效果图吧:

CoreData使用详解_第6张图片

首先,我们的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

你可能感兴趣的:(ios,数据库,数据存储,coredata)