当前,各类应用开发中只要牵扯到数据库操作通常都会用到一个概念“对象关系映射(ORM)”。例如在Java平台使用Hibernate,在.NET平台使用Entity Framework、Linq、NHibernate等。在iOS中也不例外,iOS中ORM框架首选Core Data,这是官方推荐的,不需要借助第三方框架。无论是哪种平台、哪种技术,ORM框架的作用都是相同的,那就是将关系数据库中的表(准确的说是实体)转换为程序中的对象,其本质还是对数据库的操作(例如Core Data中如果存储类型配置为SQLite则本质还是操作的SQLite数据库)。细心的朋友应该已经注意到,在上面的SQLite中其实我们在KCMainViewController中进行的数据库操作已经转换为了对象操作,服务层中的方法中已经将对数据库的操作封装起来,转换为了对Model的操作,这种方式已经是面向对象的。上述通过将对象映射到实体的过程完全是手动完成的,相对来说操作比较复杂,就拿对KCStatus对象的操作来说:首先要手动创建数据库(Status表),其次手动创建模型KCStatus,接着创建服务层KCStatusService。Core Data正是为了解决这个问题而产生的,它将数据库的创建、表的创建、对象和表的转换等操作封装起来,简化了我们的操作(注意Core Data只是将对象关系的映射简化了,并不是把服务层替代了,这一点大家需要明白)。
使用Core Data进行数据库存取并不需要手动创建数据库,这个过程完全由Core Data框架完成,开发人员面对的是模型,主要的工作就是把模型创建起来,具体数据库如何创建则不用管。在iOS项目中添加“Data Model”文件。然后在其中创建实体和关系:
模型创建的过程中需要注意:
- 实体对象不需要创建ID主键,Attributes中应该是有意义属性(创建过程中应该考虑对象的属性而不是数据库中表有几个字段,尽管多数属性会对应表的字段)。
- 所有的属性应该指定具体类型(尽管在SQLite中可以不指定),因为实体对象会对应生成ObjC模型类。
- 实体对象中其他实体对象类型的属性应该通过Relationships建立,并且注意实体之间的对应关系(例如一个用户有多条微博,而一条微博则只属于一个用户,用户和微博形成一对多的关系)。
以上模型创建后,接下来就是根据上面的模型文件(.xcdatamodeld文件)生成具体的实体类。在Xcode中添加“NSManagedObject Subclass”文件,按照步骤选择创建的模型及实体,Xcode就会根据所创建模型生成具体的实体类。
User.h
<span style="color:green;">// // User.h // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // </span><span style="color:blue;">#import </span><span style="color:#a31515;"><Foundation/Foundation.h> </span><span style="color:blue;">#import </span><span style="color:#a31515;"><CoreData/CoreData.h> </span><span style="color:black;">@</span><span style="color:blue;">class </span><span style="color:black;">Status; @</span><span style="color:blue;">interface </span><span style="color:black;">User : NSManagedObject @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * city; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * mbtype; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * name; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * profileImageUrl; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * screenName; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSSet *statuses; @end @</span><span style="color:blue;">interface </span><span style="color:black;">User (CoreDataGeneratedAccessors) - (</span><span style="color:blue;">void</span><span style="color:black;">)addStatusesObject:(Status *)value; - (</span><span style="color:blue;">void</span><span style="color:black;">)removeStatusesObject:(Status *)value; - (</span><span style="color:blue;">void</span><span style="color:black;">)addStatuses:(NSSet *)values; - (</span><span style="color:blue;">void</span><span style="color:black;">)removeStatuses:(NSSet *)values; @end</span>
User.m
<span style="color:green;">// // User.m // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // </span><span style="color:blue;">#import </span><span style="color:#a31515;">"User.h" </span><span style="color:blue;">#import </span><span style="color:#a31515;">"Status.h" </span><span style="color:black;">@implementation User @dynamic city; @dynamic mbtype; @dynamic name; @dynamic profileImageUrl; @dynamic screenName; @dynamic statuses; @end</span>
Status.h
<span style="color:green;">// // Status.h // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // </span><span style="color:blue;">#import </span><span style="color:#a31515;"><Foundation/Foundation.h> </span><span style="color:blue;">#import </span><span style="color:#a31515;"><CoreData/CoreData.h> </span><span style="color:black;">@</span><span style="color:blue;">interface </span><span style="color:black;">Status : NSManagedObject @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSDate * createdAt; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * source; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSString * text; @</span><span style="color:blue;">property </span><span style="color:black;">(nonatomic, retain) NSManagedObject *user; @end</span>
Status.m
<span style="color:green;">// // Status.m // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // </span><span style="color:blue;">#import </span><span style="color:#a31515;">"Status.h" </span><span style="color:black;">@implementation Status @dynamic createdAt; @dynamic source; @dynamic text; @dynamic user; @end</span>
很显然,通过模型生成类的过程相当简单,通常这些类也不需要手动维护,如果模型发生的变化只要重新生成即可。有几点需要注意:
- 所有的实体类型都继承于NSManagedObject,每个NSManagedObject对象对应着数据库中一条记录。
- 集合属性(例如User中的status)生成了访问此属性的分类方法。
- 使用@dynamic代表具体属性实现,具体实现细节不需要开发人员关心。
当然,了解了这些还不足以完成数据的操作。究竟Core Data具体的设计如何,要完成数据的存取我们还需要了解一下Core Data几个核心的类。
- Persistent Object Store:可以理解为存储持久对象的数据库(例如SQLite,注意Core Data也支持其他类型的数据存储,例如xml、二进制数据等)。
- Managed Object Model:对象模型,对应Xcode中创建的模型文件。
- Persistent Store Coordinator:对象模型和实体类之间的转换协调器,用于管理不同存储对象的上下文。
- Managed Object Context:对象管理上下文,负责实体对象和数据库之间的交互。
Core Data使用
Core Data使用起来相对直接使用SQLite3的API而言更加的面向对象,操作过程通常分为以下几个步骤:
1.创建管理上下文
创建管理上下可以细分为:加载模型文件->指定数据存储路径->创建对应数据类型的存储->创建管理对象上下方并指定存储。
经过这几个步骤之后可以得到管理对象上下文NSManagedObjectContext,以后所有的数据操作都由此对象负责。同时如果是第一次创建上下文,Core Data会自动创建存储文件(例如这里使用SQLite3存储),并且根据模型对象创建对应的表结构。下图为第一次运行生成的数据库及相关映射文件:
为了方便后面使用,NSManagedObjectContext对象可以作为单例或静态属性来保存,下面是创建的管理对象上下文的主要代码:
<span style="color:black;">-(NSManagedObjectContext *)createDbContext{ NSManagedObjectContext *context; </span><span style="color:green;">//打开模型文件,参数为nil则打开包中所有模型文件并合并成一个 </span><span style="color:black;">NSManagedObjectModel *model=[NSManagedObjectModel mergedModelFromBundles:nil]; </span><span style="color:green;">//创建解析器 </span><span style="color:black;">NSPersistentStoreCoordinator *storeCoordinator=[[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:model]; </span><span style="color:green;">//创建数据库保存路径 </span><span style="color:black;">NSString *dir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@</span><span style="color:#a31515;">"%@"</span><span style="color:black;">,dir); NSString *path=[dir stringByAppendingPathComponent:@</span><span style="color:#a31515;">"myDatabase.db"</span><span style="color:black;">]; NSURL *url=[NSURL fileURLWithPath:path]; </span><span style="color:green;">//添加SQLite持久存储到解析器 </span><span style="color:black;">NSError *error; [storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error]; </span><span style="color:blue;">if</span><span style="color:black;">(error){ NSLog(@</span><span style="color:#a31515;">"数据库打开失败!错误:%@"</span><span style="color:black;">,error.localizedDescription); }</span><span style="color:blue;">else</span><span style="color:black;">{ context=[[NSManagedObjectContext alloc]init]; context.persistentStoreCoordinator=storeCoordinator; NSLog(@</span><span style="color:#a31515;">"数据库打开成功!"</span><span style="color:black;">); } </span><span style="color:blue;">return </span><span style="color:black;">context; }</span>
2.查询数据
对于有条件的查询,在Core Data中是通过谓词来实现的。首先创建一个请求,然后设置请求条件,最后调用上下文执行请求的方法。
<span style="color:black;">-(</span><span style="color:blue;">void</span><span style="color:black;">)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ </span><span style="color:green;">//添加一个对象 </span><span style="color:black;">User *us= [NSEntityDescription insertNewObjectForEntityForName:@</span><span style="color:#a31515;">"User" </span><span style="color:black;">inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; </span><span style="color:green;">//保存上下文 </span><span style="color:blue;">if </span><span style="color:black;">(![self.context save:&error]) { NSLog(@</span><span style="color:#a31515;">"添加过程中发生错误,错误信息:%@!"</span><span style="color:black;">,error.localizedDescription); } }</span>
如果有多个条件,只要使用谓词组合即可,那么对于关联对象条件怎么查询呢?这里分为两种情况进行介绍:
a.查找一个对象只有唯一一个关联对象的情况,例如查找用户名为“Binger”的微博(一个微博只能属于一个用户),通过keypath查询
<span style="color:black;">-(NSArray *)getStatusesByUserName:(NSString *)name{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@</span><span style="color:#a31515;">"Status"</span><span style="color:black;">]; request.predicate=[NSPredicate predicateWithFormat:@</span><span style="color:#a31515;">"user.name=%@"</span><span style="color:black;">,name]; NSArray *</span><span style="color:blue;">array</span><span style="color:black;">=[self.context executeFetchRequest:request error:nil]; </span><span style="color:blue;">return array</span><span style="color:black;">; }</span>
此时如果跟踪Core Data生成的SQL语句会发现其实就是把Status表和User表进行了关联查询(JOIN连接)。
b.查找一个对象有多个关联对象的情况,例如查找发送微博内容中包含“Watch”并且用户昵称为“小娜”的用户(一个用户有多条微博),此时可以充分利用谓词进行过滤。
<span style="color:black;">-(NSArray *)getUsersByStatusText:(NSString *)text screenName:(NSString *)screenName{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@</span><span style="color:#a31515;">"Status"</span><span style="color:black;">]; request.predicate=[NSPredicate predicateWithFormat:@</span><span style="color:#a31515;">"text LIKE '*Watch*'"</span><span style="color:black;">,text]; NSArray *statuses=[self.context executeFetchRequest:request error:nil]; NSPredicate *userPredicate= [NSPredicate predicateWithFormat:@</span><span style="color:#a31515;">"user.screenName=%@"</span><span style="color:black;">,screenName]; NSArray *users= [statuses filteredArrayUsingPredicate:userPredicate]; </span><span style="color:blue;">return </span><span style="color:black;">users; }</span>
注意如果单纯查找微博中包含“Watch”的用户,直接查出对应的微博,然后通过每个微博的user属性即可获得用户,此时就不用使用额外的谓词过滤条件。
3.插入数据
插入数据需要调用实体描述对象NSEntityDescription返回一个实体对象,然后设置对象属性,最后保存当前上下文即可。这里需要注意,增、删、改操作完最后必须调用管理对象上下文的保存方法,否则操作不会执行。
<span style="color:black;">-(</span><span style="color:blue;">void</span><span style="color:black;">)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ </span><span style="color:green;">//添加一个对象 </span><span style="color:black;">User *us= [NSEntityDescription insertNewObjectForEntityForName:@</span><span style="color:#a31515;">"User" </span><span style="color:black;">inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; </span><span style="color:green;">//保存上下文 </span><span style="color:blue;">if </span><span style="color:black;">(![self.context save:&error]) { NSLog(@</span><span style="color:#a31515;">"添加过程中发生错误,错误信息:%@!"</span><span style="color:black;">,error.localizedDescription); } }</span>
4.删除数据
删除数据可以直接调用管理对象上下文的deleteObject方法,删除完保存上下文即可。注意,删除数据前必须先查询到对应对象。
<span style="color:black;">-(</span><span style="color:blue;">void</span><span style="color:black;">)removeUser:(User *)user{ [self.context deleteObject:user]; NSError *error; </span><span style="color:blue;">if </span><span style="color:black;">(![self.context save:&error]) { NSLog(@</span><span style="color:#a31515;">"删除过程中发生错误,错误信息:%@!"</span><span style="color:black;">,error.localizedDescription); } }</span>
5.修改数据
修改数据首先也是取出对应的实体对象,然后通过修改对象的属性,最后保存上下文。
<span style="color:black;">-(</span><span style="color:blue;">void</span><span style="color:black;">)modifyUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ User *us=[self getUserByName:name]; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; </span><span style="color:blue;">if </span><span style="color:black;">(![self.context save:&error]) { NSLog(@</span><span style="color:#a31515;">"修改过程中发生错误,错误信息:%@"</span><span style="color:black;">,error.localizedDescription); } }</span>
调试
虽然Core Data(如果使用SQLite数据库)操作最终转换为SQL操作,但是调试起来却不像操作SQL那么方便。特别是对于初学者而言经常出现查询报错的问题,如果能看到最终生成的SQL语句自然对于调试很有帮助。事实上在Xcode中是支持Core Data调试的,具体操作:Product-Scheme-Edit Scheme-Run-Arguments中依次添加两个参数(注意参数顺序不能错):-com.apple.CoreData.SQLDebug、1。然后在运行程序过程中如果操作了数据库就会将SQL语句打印在输出面板。
注意:如果模型发生了变化,此时可以重新生成实体类文件,但是所生成的数据库并不会自动更新,这时需要考虑重新生成数据库并迁移原有的数据。