1.文件系统
不管是Mac OS X 还是iOS的文件系统都是建立在UNIX文件系统基础之上的。
1.1 沙盒模型
在iOS中,一个App的读写权限只局限于自己的沙盒目录中。
沙盒模型到底有哪些好处呢?
安全:别的App无法修改你的程序或数据
保护隐私:别的App无法读取你的程序和数据
方便删除:因为一个App所有产生的内容都在自己的沙盒中,所以删除App只需要将沙盒删除就可以彻底删除程序了
iOS App沙盒中的目录
- App Bundle ,如xxx.app 其实是一个目录,里面有app本身的二进制数据以及资源文件
- Documents, 存放程序产生的文档数据
- Library , 下面默认包含下面两个目录 Caches Preferences
- tmp, 临时文件目录
如果我们想在程序中获取上面某个目录的路径,应该如何实现呢? 下面就讲讲路径的获取, 通过NSPathUtilities.h中的NSSearchPathForDirectoriesInDomains函数,我们便可以获取我们想要的路径。 此函数具体声明如下:
NSArray *NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory directory, NSSearchPathDomainMask domainMask, BOOL expandTilde);
directory 目录类型 比如Documents目录 就是NSDocumentDirectory
domainMask 在iOS的程序中这个取NSUserDomainMask
expandTilde YES,表示将~展开成完整路径
注意函数返回的类型为数组,在iOS中一般这个数组中只包含一个元素,所以直接取lastObject即可。
1.2 NSFileManager
NSFileManager提供一个类方法获得一个单例。
/* Returns the default singleton instance.*/ + (NSFileManager *)defaultManager;
下面罗列了NSFileManager的常用方法
- 新建目录
- (BOOL)createDirectoryAtPath:(NSString *)path withIntermediateDirectories:(BOOL)createIntermediates attributes:(NSDictionary *)attributes error:(NSError **)error;
createIntermediates这个参数一般为YES,表示如果目录路径中间的某个目录不存在则创建之,如果是NO的话,则要保证所创建目录的父目录都必须已经存在
- 获取目录下的所有文件
- (NSArray *)contentsOfDirectoryAtPath:(NSString *)path error:(NSError **)error;
如果目录为空,则返回空数组
其他方法:
- (BOOL)copyItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
- (BOOL)moveItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
- (BOOL)linkItemAtPath:(NSString *)srcPath toPath:(NSString *)dstPath error:(NSError **)error;
- (BOOL)removeItemAtPath:(NSString *)path error:(NSError **)error;
更多的可以查看文档 NSFileManager Class Reference。
在实际项目中,我们一般会写一个工具类来负责项目中所有的路径操作。
2. 归档(Archives) 和 序列化(Serializations)
我们经常听到“序列化”,“反序列化”这样的字眼,其实“序列化”的意思就是将对象转换成字节流以便保存或传输,“反序列化”便是一个相反的过程,从字节流转到对象。
在这节中涉及到一种文件类型plist,plist就是Property List 的缩写,即所谓的属性列表,属性列表有两种数据格式,一种是XML的,方便阅读和编辑;另一种是二进制的,节省存储空间,以及提高效率。
在Objective-C中这个对象和字节流的互转分成两类:
- 归档 普通自定义对象和字节流之间的转换
- 序列化 某些特定类型(NSDictionary, NSArray, NSString, NSDate, NSNumber,NSData)的数据和字节流之间(通常将其保存为plist文件)的转换
不过本质上讲上述两种都是对象图(Object Graph)和字节流之间的转换. Apple关于序列化和归档的编程指南: Archives and Serializations Programming Guide 。
2.1 归档
如果我们需要将自定义的一个对象保存到文件,应该如何做呢?
这里引入两个东西:一个是NSCoding协议 ;另一个是NSKeyedArchiver,NSKeyedArchiver其实继承于NSCoder,可以以键值对的方式将对象的属性进行序列化和反序列化。
具体的过程可以这样描述 通过NSKeyedArchiver 可以将实现了NSCoding协议的对象 和 字节流 相互转换 。
像一些框架中的数据类型如NSDictionary,NSArray,NSString... 都已经实现了NSCoding协议,所以可以直接对他们进行归档操作。
这里来一个比较完整的例子,一个Address类,一个User类,User类下有个Address类型的属性。
Address类
@interface Address : NSObject<NSCoding>{
NSString *country;
NSString *city;
}
@property(nonatomic,copy) NSString *country;
@property(nonatomic,copy) NSString *city;
@end
//
#import "Address.h"
@implementation Address
@synthesize country;
@synthesize city;
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:country forKey:@"country"];
[aCoder encodeObject:city forKey:@"city"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
[self setCountry:[aDecoder decodeObjectForKey:@"country"]];
[self setCity:[aDecoder decodeObjectForKey:@"city"]];
}
return self;
}
@end
User类
#import <Foundation/Foundation.h>
#import "Address.h"
@interface User : NSObject<NSCoding>{
NSString *_name;
NSString *_password;
Address *_address;
}
@property(nonatomic,copy) NSString *name;
@property(nonatomic,copy) NSString *password;
@property(nonatomic,retain) Address *address;
@end
/
#import "User.h"
@implementation User
@synthesize name = _name;
@synthesize password = _password;
@synthesize address = _address;
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:_name forKey:@"name"];
[aCoder encodeObject:_password forKey:@"password"];
[aCoder encodeObject:_address forKey:@"address"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
[self setName:[aDecoder decodeObjectForKey:@"name"]];
[self setPassword:[aDecoder decodeObjectForKey:@"password"]];
[self setAddress:[aDecoder decodeObjectForKey:@"address"]];
}
return self;
}
@end
使用示例
Address *myAddress = [[[Address alloc] init] autorelease];
myAddress.country = @"中国";
myAddress.city = @"杭州";
User *user = [[[User alloc] init] autorelease];
user.name = @"卢克";
user.password = @"lukejin";
user.address = myAddress;
[NSKeyedArchiver archiveRootObject:user toFile:@"/Users/Luke/Desktop/user"];
id object = [NSKeyedUnarchiver unarchiveObjectWithFile:@"/Users/Luke/Desktop/user"];
NSLog(@"Object Class : %@",[object class]);
通过查看文件内容可以发现,保存的是plist的二进制数据格式。 转成XML可以看到如下内容:
2.2 序列化
在实际的项目中,我们一般是将NSDictionary或NSArray的对象保存到文件或者从文件读取成对象。 当然这种只是适用于数据量不是很大的应用场景。 NSDictionary和NSArray 都有一个写入文件的方法
- (BOOL)writeToFile:(NSString *)path atomically:(BOOL)useAuxiliaryFile;
NSDictionary和NSArray会直接写成plist文件。
2.2.1 序列化的方式
序列化可以通过两种途径来进行
使用数据对象自带的方法
写文件
写完的文件内容如下:
从文件读取
NSDictionary *dictionaryFromFile = [NSDictionary dictionaryWithContentsOfFile:@"/Users/Luke/Desktop/test.plist"];
使用NSPropertyListSerialization类
通过NSPropertyListSerialization类可以将数据对象直接转成NSData或者直接写到文件或者流中去.
读取
2.2.2 User Defaults
User Defaults 顾名思义就是一个用户为系统以及程序设置的默认值。每个用户都有自己的一套数据,用户和用户之间没法共享的。
我们都知道每一个程序都会保存一些设置数据,比如记住上次窗口的位置和大小,记住是否弹出某些提示信息等。苹果提供了一个统一的解决方案,就是每一个app都有一个plist文件专门用以保存偏好设置数据。 plist文件名默认是程序Bundle identifier,扩展名为plist.
除了程序自己的设置外,系统还有一些全局的或者其它的一些设置,也属于User Defaults的范畴,User Defaults的持久化数据都保存在 ~/Library/Preferences 目录中.
这里有一点简要的说一下,User Defaults 中存放的key value分放在多个Domain中,取的时候按一定的次序取查找,次序如下:
- The Argument Domain 程序启动的时候以参数的方式传入的
- The Application Domain 通过NSUserDefaults往里面写数据的时候默认就是写到这个Domain的,通过Bundle identifier来标识
- The Global Domain 用户的全局的设置(系统的偏好设置)会放在这个Domain下,比如用户的语言设置,滚动条的设置等,里面的设置会对所有的程序起作用。
- The Languages Domains
- The Registration Domain 这个domain里面的key value是提供默认值的,一般会在程序启动的设置进行设置,他们都不会被持久化到文件的。当某个key对应的值在上面的那些domain中都不存在的时候,就到这里找。
Mac系统还为user defaults提供了很好的命令行工具,defaults 你可以通过下面的方式查看具体使用方式
man defaults
可以通过defaults domains查看当前用户的所有的domain,通过 defaults read NSGlobalDomain 读取 The Global Domain 中的所有值。
NSUserDefaults 类来读写Preferences设置,而无需考虑文件位置等细节问题。
NSUserDefaults 用起来和 NSDictionary 很相似,多了一个Domain的概念在里面。NSUserDefaults 一样提供了一个获取单例的方法.
+ (NSUserDefaults *)standardUserDefaults
NSUserDefaults提供了一系列的接口来根据key获取对应的value,搜索的次序按照上面提及到的次序在各个Domain中进行查找。还提供了一系列的 Setting Default Values的方法,这些设置的值都是在 The Application Domain 下的.当然也提供了修改其他Domain下的值的方法,只是需要整体的设置。
NSUserDefault的使用比较简单:
<span style="font-size:14px;"> NSUserDefaults *mySettingData = [NSUserDefaults standardUserDefaults]; </span>
创建NSUserDefaults对象之后即可往里面添加数据,它支持的数据类型有NSString、 NSNumber、NSDate、 NSArray、NSDictionary、BOOL、NSInteger、NSFloat等系统定义的数据类型,如果要存放自定义的对象(如自定义的类对象),则必须将其转换成NSData存储:
<span style="font-size:14px;"> NSArray *arr = [[NSArray alloc] initWithObjects:@"arr1", @"arr2", nil]
[mySettingData setObject:arr forKey:@"arrItem"];
[mySettingData setObject:@"admin" forKey:@"user_name"];
[mySettingData setBOOL:@YES forKey:@"auto_login"];
[mySettingData setInteger:1 forKey:@"count"]; </span>
往NSUserDefaults添加数据后,它们就变成了全局的变量,App中即可读写NSUserDefault中的数据:
<span style="font-size:14px;"> NSUserDefaults *mySettingDataR = [NSUserDefaults standardUserDefaults];
NSLog(@"arrItem=%@", [mySettingDataR objectForKey:@"arrItem"]);
NSLog(@"user_name=%@", [mySettingDataR objectForKey:@"user_name"]);
NSLog(@"count=%d", [mySettingDataR integerForKey:@"count"]); </span>
如果想删除某个数据项,可以使用removeObjectForKey删除数据:
<span style="font-size:14px;"> [mySettingData removeObjectForKey:@"arrItem"]; </span>
需要注意的是,NSUserDefaults是定时把缓存中的数据写入磁盘的,而不是即时写入,为了防止在写完NSUserDefaults后程序退出导致的数据丢失,可以在写入数据后使用synchronize强制立即将数据写入磁盘:
<span style="font-size:14px;">[mySettingData synchronize]; </span>
运行上面的语句后,NSUserDefaults中的数据即被写入到.plist文件中,如果是在模拟器上运行程序,可以在Mac的/Users/YOUR-USERNAME/Library/Application Support/iPhone Simulator/4.1/Applications/YOUR-APP-DIR/Library/Prefereces目录下面找到一个文件名为YOUR-Bundle_Identifier.plist的plist文件,用Xcode打开该文件,可以看到刚才写入的数据。
3. sqlite和FMDB
3.1 sqlite
SQLite是目前主流的嵌入式关系型数据库,其最主要的特点就是轻量级、跨平台,当前很多嵌入式操作系统都将其作为数据库首选。虽然SQLite是一款轻型数据库,但是其功能也绝不亚于很多大型关系数据库。学习数据库就要学习其相关的定义、操作、查询语言,也就是大家日常说得SQL语句。和其他数据库相比,SQLite中的SQL语法并没有太大的差别,因此这里对于SQL语句的内容不会过多赘述,大家可以参考SQLite中其他SQL相关的内容,这里还是重点讲解iOS中如何使用SQLite构建应用程序。先看一下SQLite数据库的几个特点:
- 基于C语言开发的轻型数据库
- 在iOS中需要使用C语言语法进行数据库操作、访问(无法使用ObjC直接访问,因为libsqlite3框架基于C语言编写)
- SQLite中采用的是动态数据类型,即使创建时定义了一种类型,在实际操作时也可以存储其他类型,但是推荐建库时使用合适的类型(特别是应用需要考虑跨平台的情况时)
- 建立连接后通常不需要关闭连接(尽管可以手动关闭)
要使用SQLite很简单,如果在Mac OSX上使用可以考虑到SQLite官方网站下载命令行工具,也可以使用类似于SQLiteManager、Navicat for SQLite等工具。为了方便大家开发调试,建议在开发环境中安装上述工具。
在iOS中操作SQLite数据库可以分为以下几步(注意先在项目中导入libsqlite3框架):
- 打开数据库,利用sqlite3_open()打开数据库会指定一个数据库文件保存路径,如果文件存在则直接打开,否则创建并打开。打开数据库会得到一个sqlite3类型的对象,后面需要借助这个对象进行其他操作。
- 执行SQL语句,执行SQL语句又包括有返回值的语句和无返回值语句。
- 对于无返回值的语句(如增加、删除、修改等)直接通过sqlite3_exec()函数执行;
- 对于有返回值的语句则首先通过sqlite3_prepare_v2()进行sql语句评估(语法检测),然后通过sqlite3_step()依次取出查询结果的每一行数据,对于每行数据都可以通过对应的sqlite3_column_类型()方法获得对应列的数据,如此反复循环直到遍历完成。当然,最后需要释放句柄。
在整个操作过程中无需管理数据库连接,对于嵌入式SQLite操作是持久连接(尽管可以通过sqlite3_close()关闭),不需要开发人员自己释放连接。纵观整个操作过程,其实与其他平台的开发没有明显的区别,较为麻烦的就是数据读取,在iOS平台中使用C进行数据读取采用了游标的形式,每次只能读取一行数据,较为麻烦。因此实际开发中不妨对这些操作进行封装:
KCDbManager.h
// // DbManager.h // DataAccess // // Created by Kenshin Cui on 14-3-29. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import <Foundation/Foundation.h> #import <sqlite3.h> #import "KCSingleton.h" @interface KCDbManager : NSObject singleton_interface(KCDbManager); #pragma mark - 属性 #pragma mark 数据库引用,使用它进行数据库操作 @property (nonatomic) sqlite3 *database; #pragma mark - 共有方法 /** * 打开数据库 * * @param dbname 数据库名称 */ -(void)openDb:(NSString *)dbname; /** * 执行无返回值的sql * * @param sql sql语句 */ -(void)executeNonQuery:(NSString *)sql; /** * 执行有返回值的sql * * @param sql sql语句 * * @return 查询结果 */ -(NSArray *)executeQuery:(NSString *)sql; @end
KCDbManager.m
// // DbManager.m // DataAccess // // Created by Kenshin Cui on 14-3-29. // Copyright (c) 2014年 Kenshin Cui. All rights reserved. // #import "KCDbManager.h" #import <sqlite3.h> #import "KCSingleton.h" #import "KCAppConfig.h" #ifndef kDatabaseName #define kDatabaseName @"myDatabase.db" #endif @interface KCDbManager() @end @implementation KCDbManager singleton_implementation(KCDbManager) #pragma mark 重写初始化方法 -(instancetype)init{ KCDbManager *manager; if((manager=[super init])) { [manager openDb:kDatabaseName]; } return manager; } -(void)openDb:(NSString *)dbname{ //取得数据库保存路径,通常保存沙盒Documents目录 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //如果有数据库则直接打开,否则创建并打开(注意filePath是ObjC中的字符串,需要转化为C语言字符串类型) if (SQLITE_OK ==sqlite3_open(filePath.UTF8String, &_database)) { NSLog(@"数据库打开成功!"); }else{ NSLog(@"数据库打开失败!"); } } -(void)executeNonQuery:(NSString *)sql{ char *error; //单步执行sql语句,用于插入、修改、删除 if (SQLITE_OK!=sqlite3_exec(_database, sql.UTF8String, NULL, NULL,&error)) { NSLog(@"执行SQL语句过程中发生错误!错误信息:%s",error); } } -(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *rows=[NSMutableArray array];//数据行 //评估语法正确性 sqlite3_stmt *stmt; //检查语法正确性 if (SQLITE_OK==sqlite3_prepare_v2(_database, sql.UTF8String, -1, &stmt, NULL)) { //单步执行sql语句 while (SQLITE_ROW==sqlite3_step(stmt)) { int columnCount= sqlite3_column_count(stmt); NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<columnCount; i++) { const char *name= sqlite3_column_name(stmt, i);//取得列名 const unsigned char *value= sqlite3_column_text(stmt, i);//取得某列的值 dic[[NSString stringWithUTF8String:name]]=[NSString stringWithUTF8String:(const char *)value]; } [rows addObject:dic]; } } //释放句柄 sqlite3_finalize(stmt); return rows; } @end
在上面的类中对于数据库操作进行了封装,封装之后数据操作更加方便,同时所有的语法都由C转换成了ObjC。
3.2 FMDB
相比于SQLite3来说Core Data存在着诸多优势,它面向对象,开发人员不必过多的关心更多数据库操作知识,同时它基于ObjC操作,书写更加优雅等。但是它本身也存在着一定的限制,例如如果考虑到跨平台,则只能选择SQLite,因为无论是iOS还是Android都可以使用同一个数据库,降低了开发成本和维护成本。其次是当前多数ORM框架都存在的性能问题,因为ORM最终转化为SQL操作,其中牵扯到模型数据转化,其性能自然比不上直接使用SQL操作数据库。那么有没有更好的选择呢?答案就是对SQLite进行封装。
其实通过前面对于SQLite的分析,大家应该已经看到KCDbManager就是对于SQLite封装的结果,开发人员面对的只有SQL和ObjC方法,不用过多libsqlite3的C语言API。但它毕竟只是一个简单的封装,还有更多的细节没有考虑,例如如何处理并发安全性,如何更好的处理事务等。因此,这里推荐使用第三方框架FMDB,整个框架非常轻量级但又不失灵活性,也是很多企业开发的首选。
1.FMDB既然是对于libsqlite3框架的封装,自然使用起来也是类似的,使用前也要打开一个数据库,这个数据库文件存在则直接打开否则会创建并打开。这里FMDB引入了一个MFDatabase对象来表示数据库,打开数据库和后面的数据库操作全部依赖此对象。下面是打开数据库获得MFDatabase对象的代码:
-(void)openDb:(NSString *)dbname{ //取得数据库保存路径,通常保存沙盒Documents目录 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //创建FMDatabase对象 self.database=[FMDatabase databaseWithPath:filePath]; //打开数据上 if ([self.database open]) { NSLog(@"数据库打开成功!"); }else{ NSLog(@"数据库打开失败!"); } }
注意:dataWithPath中的路径参数一般会选择保存到沙箱中的Documents目录中;如果这个参数设置为nil则数据库会在内存中创建;如果设置为@””则会在沙箱中的临时目录创建,应用程序关闭则文件删除。
2.对于数据库的操作跟前面KCDbManager的封装是类似的,在FMDB中FMDatabase类提供了两个方法executeUpdate:和executeQuery:分别用于执行无返回结果的查询和有返回结果的查询。当然这两个方法有很多的重载这里就不详细解释了。唯一需要指出的是,如果调用有格式化参数的sql语句时,格式化符号使用“?”而不是“%@”、等。下面是两种情况的代码片段:
a.无返回结果
-(void)executeNonQuery:(NSString *)sql{ //执行更新sql语句,用于插入、修改、删除 if (![self.database executeUpdate:sql]) { NSLog(@"执行SQL语句过程中发生错误!"); } }
b.有返回结果
-(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *array=[NSMutableArray array]; //执行查询sql语句 FMResultSet *result= [self.database executeQuery:sql]; while (result.next) { NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<result.columnCount; ++i) { dic[[result columnNameForIndex:i]]=[result stringForColumnIndex:i]; } [array addObject:dic]; } return array; }
对于有返回结果的查询而言,查询完返回一个游标FMResultSet,通过遍历游标进行查询。而且FMDB中提供了大量intForColumn、stringForColumn等方法进行取值。
并发和事务
我们知道直接使用libsqlite3进行数据库操作其实是线程不安全的,如果遇到多个线程同时操作一个表的时候可能会发生意想不到的结果。为了解决这个问题建议在多线程中使用FMDatabaseQueue对象,相比FMDatabase而言,它是线程安全的。
创建FMDatabaseQueue的方法是类似的,调用databaseQueueWithPath:方法即可。注意这里不需要调用打开操作。
-(void)openDb:(NSString *)dbname{ //取得数据库保存路径,通常保存沙盒Documents目录 NSString *directory=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",directory); NSString *filePath=[directory stringByAppendingPathComponent:dbname]; //创建FMDatabaseQueue对象 self.database=[FMDatabaseQueue databaseQueueWithPath:filePath]; }
然后所有的增删改查操作调用FMDatabaseQueue的inDatabase:方法在block中执行操作sql语句即可。
-(void)executeNonQuery:(NSString *)sql{ //执行更新sql语句,用于插入、修改、删除 [self.database inDatabase:^(FMDatabase *db) { [db executeQuery:sql]; }]; } -(NSArray *)executeQuery:(NSString *)sql{ NSMutableArray *array=[NSMutableArray array]; [self.database inDatabase:^(FMDatabase *db) { //执行查询sql语句 FMResultSet *result= [db executeQuery:sql]; while (result.next) { NSMutableDictionary *dic=[NSMutableDictionary dictionary]; for (int i=0; i<result.columnCount; ++i) { dic[[result columnNameForIndex:i]]=[result stringForColumnIndex:i]; } [array addObject:dic]; } }]; return array; }
之所以将事务放到FMDB中去说并不是因为只有FMDB才支持事务,而是因为FMDB将其封装成了几个方法来调用,不用自己写对应的sql而已。其实在在使用libsqlite3操作数据库时也是原生支持事务的(因为这里的事务是基于数据库的,FMDB还是使用的SQLite数据库),只要在执行sql语句前加上“begin transaction;”执行完之后执行“commit transaction;”或者“rollback transaction;”进行提交或回滚即可。另外在Core Data中大家也可以发现,所有的增、删、改操作之后必须调用上下文的保存方法,其实本身就提供了事务的支持,只要不调用保存方法,之前所有的操作是不会提交的。在FMDB中FMDatabase有beginTransaction、commit、rollback三个方法进行开启事务、提交事务和回滚事务。
4. Core Data
当前,各类应用开发中只要牵扯到数据库操作通常都会用到一个概念“对象关系映射(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
// // User.h // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import <Foundation/Foundation.h> #import <CoreData/CoreData.h> @class Status; @interface User : NSManagedObject @property (nonatomic, retain) NSString * city; @property (nonatomic, retain) NSString * mbtype; @property (nonatomic, retain) NSString * name; @property (nonatomic, retain) NSString * profileImageUrl; @property (nonatomic, retain) NSString * screenName; @property (nonatomic, retain) NSSet *statuses; @end @interface User (CoreDataGeneratedAccessors) - (void)addStatusesObject:(Status *)value; - (void)removeStatusesObject:(Status *)value; - (void)addStatuses:(NSSet *)values; - (void)removeStatuses:(NSSet *)values; @end
User.m
// // User.m // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "User.h" #import "Status.h" @implementation User @dynamic city; @dynamic mbtype; @dynamic name; @dynamic profileImageUrl; @dynamic screenName; @dynamic statuses; @end
Status.h
// // Status.h // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import <Foundation/Foundation.h> #import <CoreData/CoreData.h> @interface Status : NSManagedObject @property (nonatomic, retain) NSDate * createdAt; @property (nonatomic, retain) NSString * source; @property (nonatomic, retain) NSString * text; @property (nonatomic, retain) NSManagedObject *user; @end
Status.m
// // Status.m // CoreData // // Created by Kenshin Cui on 14/03/27. // Copyright (c) 2014年 cmjstudio. All rights reserved. // #import "Status.h" @implementation Status @dynamic createdAt; @dynamic source; @dynamic text; @dynamic user; @end
很显然,通过模型生成类的过程相当简单,通常这些类也不需要手动维护,如果模型发生的变化只要重新生成即可。有几点需要注意:
- 所有的实体类型都继承于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对象可以作为单例或静态属性来保存,下面是创建的管理对象上下文的主要代码:
-(NSManagedObjectContext *)createDbContext{ NSManagedObjectContext *context; //打开模型文件,参数为nil则打开包中所有模型文件并合并成一个 NSManagedObjectModel *model=[NSManagedObjectModel mergedModelFromBundles:nil]; //创建解析器 NSPersistentStoreCoordinator *storeCoordinator=[[NSPersistentStoreCoordinator alloc]initWithManagedObjectModel:model]; //创建数据库保存路径 NSString *dir=[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) firstObject]; NSLog(@"%@",dir); NSString *path=[dir stringByAppendingPathComponent:@"myDatabase.db"]; NSURL *url=[NSURL fileURLWithPath:path]; //添加SQLite持久存储到解析器 NSError *error; [storeCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:url options:nil error:&error]; if(error){ NSLog(@"数据库打开失败!错误:%@",error.localizedDescription); }else{ context=[[NSManagedObjectContext alloc]init]; context.persistentStoreCoordinator=storeCoordinator; NSLog(@"数据库打开成功!"); } return context; }
2.查询数据
对于有条件的查询,在Core Data中是通过谓词来实现的。首先创建一个请求,然后设置请求条件,最后调用上下文执行请求的方法。
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ //添加一个对象 User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; //保存上下文 if (![self.context save:&error]) { NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription); } }
如果有多个条件,只要使用谓词组合即可,那么对于关联对象条件怎么查询呢?这里分为两种情况进行介绍:
a.查找一个对象只有唯一一个关联对象的情况,例如查找用户名为“Binger”的微博(一个微博只能属于一个用户),通过keypath查询
-(NSArray *)getStatusesByUserName:(NSString *)name{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@"Status"]; request.predicate=[NSPredicate predicateWithFormat:@"user.name=%@",name]; NSArray *array=[self.context executeFetchRequest:request error:nil]; return array; }
此时如果跟踪Core Data生成的SQL语句会发现其实就是把Status表和User表进行了关联查询(JOIN连接)。
b.查找一个对象有多个关联对象的情况,例如查找发送微博内容中包含“Watch”并且用户昵称为“小娜”的用户(一个用户有多条微博),此时可以充分利用谓词进行过滤。
-(NSArray *)getUsersByStatusText:(NSString *)text screenName:(NSString *)screenName{ NSFetchRequest *request=[NSFetchRequest fetchRequestWithEntityName:@"Status"]; request.predicate=[NSPredicate predicateWithFormat:@"text LIKE '*Watch*'",text]; NSArray *statuses=[self.context executeFetchRequest:request error:nil]; NSPredicate *userPredicate= [NSPredicate predicateWithFormat:@"user.screenName=%@",screenName]; NSArray *users= [statuses filteredArrayUsingPredicate:userPredicate]; return users; }
注意如果单纯查找微博中包含“Watch”的用户,直接查出对应的微博,然后通过每个微博的user属性即可获得用户,此时就不用使用额外的谓词过滤条件。
3.插入数据
插入数据需要调用实体描述对象NSEntityDescription返回一个实体对象,然后设置对象属性,最后保存当前上下文即可。这里需要注意,增、删、改操作完最后必须调用管理对象上下文的保存方法,否则操作不会执行。
-(void)addUserWithName:(NSString *)name screenName:(NSString *)screenName profileImageUrl:(NSString *)profileImageUrl mbtype:(NSString *)mbtype city:(NSString *)city{ //添加一个对象 User *us= [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:self.context]; us.name=name; us.screenName=screenName; us.profileImageUrl=profileImageUrl; us.mbtype=mbtype; us.city=city; NSError *error; //保存上下文 if (![self.context save:&error]) { NSLog(@"添加过程中发生错误,错误信息:%@!",error.localizedDescription); } }
4.删除数据
删除数据可以直接调用管理对象上下文的deleteObject方法,删除完保存上下文即可。注意,删除数据前必须先查询到对应对象。
-(void)removeUser:(User *)user{ [self.context deleteObject:user]; NSError *error; if (![self.context save:&error]) { NSLog(@"删除过程中发生错误,错误信息:%@!",error.localizedDescription); } }
5.修改数据
修改数据首先也是取出对应的实体对象,然后通过修改对象的属性,最后保存上下文。
-(void)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; if (![self.context save:&error]) { NSLog(@"修改过程中发生错误,错误信息:%@",error.localizedDescription); } }
调试
虽然Core Data(如果使用SQLite数据库)操作最终转换为SQL操作,但是调试起来却不像操作SQL那么方便。特别是对于初学者而言经常出现查询报错的问题,如果能看到最终生成的SQL语句自然对于调试很有帮助。事实上在Xcode中是支持Core Data调试的,具体操作:Product-Scheme-Edit Scheme-Run-Arguments中依次添加两个参数(注意参数顺序不能错):-com.apple.CoreData.SQLDebug、1。然后在运行程序过程中如果操作了数据库就会将SQL语句打印在输出面板。
注意:如果模型发生了变化,此时可以重新生成实体类文件,但是所生成的数据库并不会自动更新,这时需要考虑重新生成数据库并迁移原有的数据。