iOS App开发中数据持久层一般用的是FMDB,FMDB是对SQLite的一层Wrapper,封装了比较难懂的C接口,提供了面向Objective-C的接口方法,对iOS开发者很友好。
正如FMDB源码注释中所说的那样FMDB主要有3个类:
FMDatabase
SQLite数据库,用于执行SQL语句。
FMResultSet
在FMDatabase对象上执行查询语句得到的结果集
FMDatabaseQueue
用于多线程数据库在的查询、更新
FMDatabase
FMDatabase描述的是SQLite数据库,它负责管理数据库的创建、打开、关闭、事物、CRUD语句执行。
创建数据库
创建数据库时只需指定数据库文件路径即可,具体路径可为以下3种之一:
- 文件系统路径,这个文件可以不存在,如果文件不存在就创建。
- 一个空字符串
@“”,此时会在临时目录下创建一个空数据库,这个数据库会在数据库链接关闭时删除。 nil,会创建一个内存数据库,这个数据库会在数据库链接关闭时销毁。
FMDatabase提供了2个方法供使用者创建数据库,一个是类方法,另一个是实例方法,类方法最终调用的也是实例方法,默认初始化方法中主要初始化了数据库路径、查询结果集合、最大忙重试时间。
打开数据库
FMDB提供了3个打开数据库的方法,其中open方法调用的是sqlite3_open()函数,-(BOOL)openWithFlags:(int)flags、(BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName调用的是sqlite3_open_v2函数,其中sqlite3_open_v2函数可设置打开模式及虚拟文件系统名称,其中打开模式有SQLITE_OPEN_READONLY、SQLITE_OPEN_READWRITE、SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,数据库打开后设置忙重试回调sqlite3_busy_handler,回调方法如果返回0就不在重试。
CRUD
在FMDB中把CRUD分为了2种操作:更新(没有结果返回,比如UPDATE、INSERT、DELETE)、查询(有结果返回,比如QUERY)
- 更新操作
FMDB中所有的更新方法最终调用的都是(BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args方法,该方法调用了SQLite的3个C接口API,分别为:
sqlite3_prepare_v2为了方便SQLite处理SQL语句,需要先把字符串预处理成sqlite3_stmt
sqlite3_bind把参数绑定到SQLite中?预置符位置
sqlite_step执行SQLite更新语句
在执行更新语句中为了提高效率(sqlite3_prepare_v2很耗时),增加了sqlite3_stmt预处理语句缓存机制,如果使能了_shouldCacheStatements属性,那么经过预处理的SQL语句会被缓存起来,等下次再使用同样的SQL语句时执行从缓存中获取,获取到后调用sqlite3_reset函数就可以继续绑定新的参数。
绑定参数主要是根据参数的数据类型调用相关的bind函数,如何判断参数是何种类型?根据objCType方法返回的类型字符串判断,例如:
1 | if (strcmp([obj objCType], @encode(int)) == 0) { sqlite3_bind_int(pStmt, idx, [obj intValue]); } |
如果参数是NSData类型,那么调用sqlite3_bind_blob,如果是int类型,调用sqlite3_bind_int,FMDB中除了NSString, NSNumber, NSNull, NSDate, 和 NSData之外其它对象类型均被认为是text类型,调用对象的description方法,具体参考源码中(void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt方法。
- 查询操作
查询操作与上述更新操作有部分类似,也会调用sqlite3_prepare_v2和sqlite3_bind接口,但是查询操作不会立即执行sqlite_step,只有在FMResultSet上调用next方法时才会执行,查询操作会返回FMResultSet对象。
关闭数据库
关闭数据库主要是释放申请的相关资源,包括清空缓存语句、关闭查询结果、关闭数据库(用的是sqlite3_close)。
事物
当想要原子操作数据库时可使用事物,FMDB提供了立即执行和延迟执行的事物,SQL语句分别为begin exclusive transaction和begin deferred transaction,当想要提交事物时执行commit transaction语句,当想要回滚时执行rollback transaction语句,上述代码逻辑具体参考源码中以下方法:
1 | - (BOOL)beginTransaction; - (BOOL)beginDeferredTransaction; - (BOOL)commit; - (BOOL)rollback; |
FMResultSet
FMResultSet是查询结果集,对外主要提供了2大类查询方法,一类是根据列索引查(xxxForColumnIndex,xxx表示的是具体类型),另一类是根据列名称查询(xxxForColumn,xxx表示的是具体类型)。实际上根据列名称查询方法最终调用的是根据列索引查询的方法,大概的实现逻辑为:FMResultSet内部维护了一个列名到列索引的映射表,当调用列名称时先通过列名到列索引的映射表查到相应的列索引号,然后调用列索引查询方法(列索引方法调用的是sqlite3_column_xx),那列名到列索引的映射表是怎么构造的呢?先根据sqlite3_column_count获取到列数,然后遍历每一列,再根据sqlite3_column_name获取列名即可完成映射,具体代码如下:
1 | - (NSMutableDictionary *)columnNameToIndexMap { if (!_columnNameToIndexMap) { int columnCount = sqlite3_column_count([_statement statement]); _columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount]; int columnIdx = 0; for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { [_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx] forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]]; } } return _columnNameToIndexMap; } |
FMDatabaseQueue
FMDatabase源码中写的很清楚,在多线程中用一个<FMDatabase>实例是坏想法,<FMDatabase>不是线程安全的,想要在多线程上操作数据库就用FMDatabaseQueue,FMDatabaseQueue会在内部的一个串行对列上执行blocks代码块,因此即便你在同一时间多个线程上调用FMDatabaseQueue方法,这些查询和更新操作也不会混乱,它们会根据顺讯依次执行。
FMDatabaseQueue主要功能就是提供了一个内部串行对列,确保了多线程安全,具体的数据库操作逻辑还是用的FMDatabase,FMDatabaseQueue在执行blocks时是同步执行的,所以有可能引发死锁,为了解决死锁FMDatabase在创建内部对列时通过dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);方法设置了一个context,当在执行block时先判断当前Queue的context,如果和之前设置的context相等则发生死锁,通过assert提示使用者发生死锁,例如如下代码就会发生死锁:
1 | FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:[NSTemporaryDirectory() stringByAppendingString:@"temp.db"]]; [queue inDatabase:^(FMDatabase *db) { NSLog(@"in queue"); //在同一个串行对列中同步执行,故而发生死锁 [queue inDatabase:^(FMDatabase *db) { NSLog(@"in nested queue"); }]; }] |
切记不要在FMDatabase的block中嵌套调用。