IOS CoreData

一、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博客。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值