介绍
FMDB是一个基于SQLite的数据库框架;使用OC语言对SQLite3的C语言接口做了一层面向对象的封装;框架代码本身比较简单,对比苹果自身的数据库框架Core Data
来说,更加的轻量级和灵活;
FMDB
封装了操作数据库可能会使用到的一些接口,包括最基本的CURD,以及事物和保存点等;并通过一个GCD的Serial队列保证在多线程环境下的数据安全;
源码解析
FMDB本身的代码不是很复杂,主要包括以下几个组成文件:
1、FMDatabase:它与数据库文件是一一对应的,在新建一个FMDatabase对象时,可以关联一个已有的数据库文件;在对象中封装了操作数据库的基本接口,如增删改查等;
2、FMDatabaseAdditions:是FMDatabase的一个Category分类,封装了一些数据库的便捷功能;如验证SQL语句的正确性、判断一个表或者是表中的某一行是否存在等;
3、FMDatabaseQueue:当需要在多线程环境下执行数据库操作时,就需要通过FMDatabaseQueue初始化数据库对象,通过FMDatabaseQueue的接口执行数据库操作能保证多线程环境下的访问安全;
4、FMDatabasePool:一个FMDatabase的对象池封装,在多线程环境中访问单个FMDatabase对象是很容易引起问题的,可以通过FMDatabasePool对象池来解决多线程下的访问安全问题;但是FMDB建议我们优先使用FMDatabaseQueue;除非确实不得已才去使用FMDatabasePool,在此情况下还需要注意避免使用时产生死锁问题;
5、FMResultSet:封装了select语句的结果集,可以通过它遍历查询的结果集,获取出查询得到的数据;
内部实现与框架的基本使用
我们主要看一下FMDatabase
、FMDatabaseQueue
和FMResultSet
这三个类的关键实现;
先从FMDatabase开始,在这个类的.h文件介绍说明中有如下描述:
An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three:
1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you.
2. An empty string (`@""`). An empty database is created at a temporary location. This database is deleted with the `FMDatabase` connection is closed.
3. `nil`. An in-memory database is created. This database will be destroyed with the `FMDatabase` connection is closed.
For example, to create/open a database in your Mac OS X `tmp` folder:
FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];
Or, in iOS, you might open a database in the app's `Documents` directory:
NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docsPath stringByAppendingPathComponent:@"test.db"];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
(For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [http://www.sqlite.org/inmemorydb.html](http://www.sqlite.org/inmemorydb.html))
通过以上介绍我们知道,FMDatabase
是通过一个SQLite数据库文件的路径创建出来的对象;文件的路径可以是以下三种:
1、一个具体的文件路径;如果路径上没有对应的文件,系统会帮我们在路径上创建一个文件;
2、如果路径是@""
这样的空字符串,系统会在临时文件夹内创建一个空数据库,当使用完成关闭数据库连接时,这个临时文件会被删除;
3、如果路径为nil
则会在内存中创建一个数据库,当数据库连接关闭时,内存中的数据库会被删除;
以下是FMDatabase的初始化方法:
- (instancetype)initWithPath:(NSString *)path {
//确保在开始前sqlite3是线程安全的状态
assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do.
self = [super init];
if (self) {
_databasePath = [path copy]; //记录书库的路径
_openResultSets = [[NSMutableSet alloc] init]; //存放查询语句结果集的集合对象
_db = nil; //数据库句柄,当调用open函数后,这个指针会被赋值,指向数据库对象
_logsErrors = YES; //设置数据库操作出错时,打印日志信息
_crashOnErrors = NO; //关闭访问出错让程序退出的开关
_maxBusyRetryTimeInterval = 2; //当数据库被上锁后,每隔2秒后重新尝试访问
}
return self;
}
初始化方法内只对一些基础变量进行了赋值,并没有涉及到数据库的操作,初始化完成后可以通open
或openWithFlags: vfs:
方法打开数据库;数据库操作结束后,通过调用close
方法关闭数据库连接;
在FMDatabase中还封装了一个FMStatement
内部类,这个类主要用在框架内部,用于把一个SQL语句封装成一个对象;
在FMDatabase中主要封装了两种类型的数据库操作方法,分别是executeUpdate
和executeQuery
系列,其中executeUpdate系方法用于数据的增、删、改操作,executeQuery系方法用于查询操作;数据库的事物和保存点等操作也是通过调用executeUpdate
系方法实现的;
/** 以下接收不同类型的参数输入,并最终执行一条SQL语句,完成增、删、改等操作 */
- (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError * _Nullable *)outErr, ...;
- (BOOL)executeUpdate:(NSString*)sql, ...;
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments;
- (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args;
/** 以下方法执行多条SQL语句;参数sql是通过“;”拼接起来的多条语句组合而成的字符串 */
- (BOOL)executeStatements:(NSString *)sql;
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(__attribute__((noescape)) FMDBExecuteStatementsCallbackBlock _Nullable)block;
/** 以下接收不同类型的参数输入,并执行一条SQL查询语句返回结果集 */
- (FMResultSet * _Nullable)executeQuery:(NSString*)sql, ...;
- (FMResultSet * _Nullable)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary * _Nullable)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withVAList:(va_list)args;
/** 事物相关的接口 */
- (BOOL)beginTransaction; //Begin a transaction
- (BOOL)beginDeferredTransaction; //Begin a deferred transaction
- (BOOL)commit; //Commit a transaction
- (BOOL)rollback; //Rollback a transaction
更新与查询的接口几乎是一一对应的,都是接收一条SQL语句然后执行并得到执行结果;由于SQL语句中会使用“?”做参数的占位符,然后根据传入的参数补充出完整的SQL语句,不同的方法提供了不同的方式接受SQL语句的参数传入;下面看一个调用的例子:
//创建数据库对象
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
if (![db open]) { //打开数据库
NSLog(@"Could not open db.");
return 0;
}
//通过executeUpdate语句创建一张新的表
[db executeUpdate:@"create table test (a text, b text, c integer, d double, e double)"];
[db beginTransaction]; //开始一个事物,即通过使用事物的方式更新数据库的数据
int i = 0;
while (i++ < 20) {
//在数据库中插入数据
[db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" ,
@"hi'", // look! I put in a ', and I'm not escaping it!
[NSString stringWithFormat:@"number %d", i],
[NSNumber numberWithInt:i],
[NSDate date],
[NSNumber numberWithFloat:2.2f]];
}
[db commit]; //提交一个事物
在executeUpdate
方法内部首先会通过调用sqlite3_prepare_v2
函数执行传入的sql语句参数并生成一个sqlite3_stmt *pStmt
指针;然后根据不同的executeUpdate
方法解析传递给sql语句的参数并把参数绑定到pStmt
对象中,在通过sqlite3_step(pStmt)
函数执行处理好的SQL语句,如果是查询语句会同时返回查询的结果集;中间如果有任何环节出现错误都会通过sqlite3_finalize(pStmt)
语句释放这个预备好的SQL语句;
在FMDatabase的executeQuery
系的方法内部,会把select语句的结果集通过FMResultSet
对象进行处理;看一下掉用到FMResultSet
对象的过程:
// the statement gets closed in rs's dealloc or [rs close];
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self]; //rs是FMResultSet类型的对象
[rs setQuery:sql];
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet]; //在FMDatabase中缓存查询后的结果集
以上代码的statement
参数是FMStatement
的对象,在处理好SQL语句后会生成statement
对象,然后把对象传递给FMResultSet方法内部,去执行查询操作并得到查询的结果;看一下FMResultSet的内部方法实现:
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
FMResultSet *rs = [[FMResultSet alloc] init]; //生成结果集对象
[rs setStatement:statement]; //设置好SQL语句
[rs setParentDB:aDB]; //设置好数据库句柄
NSParameterAssert(![statement inUse]);
[statement setInUse:YES]; // weak reference
return FMDBReturnAutoreleased(rs);
}
方法把数据库的句柄和拼接好的SQL语句都传递到了FMResultSet对象中;之后可以通过调用FMResultSet对象的next
方法遍历查询结果集,在next
方法的内部通过调用sqlite3_step([_statement statement])
执行这个查询语句;通过FMResultSet遍历结果集的调用过程如下:
//通过执行executeQuery语句,返回FMResultSet对象(查询结果集)
FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
while ([rs next]) { //调用next方法逐行便利结果集,并打印每一行的数据
// just print out what we've got in a number of formats.
NSLog(@"%d %@ %@ %@ %@ %f %f",
[rs intForColumn:@"c"],
[rs stringForColumn:@"b"],
[rs stringForColumn:@"a"],
[rs stringForColumn:@"rowid"],
[rs dateForColumn:@"d"],
[rs doubleForColumn:@"d"],
[rs doubleForColumn:@"e"]);
}
// close the result set.
// it'll also close when it's dealloc'd, but we're closing the database before
// the autorelease pool closes, so sqlite will complain about it.
[rs close]; //对象被dealloc之前,先通过close方法关闭和这个sql语句有关的资源
以上代码通过FMResultSet对象遍历了select
语句的结果集,其中不同类型的column数据读取在FMResultSet中都有对应的取值方法;在FMResultSet中还包括一些其他的便捷方法,如通过列的index获取columnName,或者通过columnName获取列的index等;
线程安全
在FMDB框架中,除了有基本的数据库操作外,还提供了多线程环境下的安全访问机制;我们看一下FMDatabaseQueue
中是如何保证多线程下的安全访问的;
先看一下FMDatabaseQueue
这个类的“.h”文件说明:
To perform queries and updates on multiple threads, you'll want to use `FMDatabaseQueue`.
Using a single instance of `<FMDatabase>` from multiple threads at once is a bad idea. It has always been OK to make a `<FMDatabase>` object *per thread*. Just don't share a single instance across threads, and definitely not across multiple threads at the same time.
Instead, use `FMDatabaseQueue`. Here's how to use it:
First, make your queue.
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
Then use it like so:
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
//…
}
}];
An easy way to wrap things up in a transaction can be done like this:
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// etc…
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];
`FMDatabaseQueue` will run the blocks on a serialized queue (hence the name of the class). So if you call `FMDatabaseQueue`'s methods from multiple threads at the same time, they will be executed in the order they are received. This way queries and updates won't step on each other's toes, and every one is happy.
以上说明的大概意思是说,FMDatabase
对象只适合在单线程per thread
中使用,再多线程下是不安全的;多线程环境下应该使用FMDatabaseQueue
对象替代,以保证数据库的安全访问;
使用FMDatabaseQueue
进行数据库操作时,会把对应的操作封装成block
的形式添加到一个serial
的线程队列中,以保证所有异步添加进来的数据库操作都会在serial队列中按顺序执行,保证多线程环境下的数据安全;
也就是在FMDatabaseQueue
中的多线程,实际上只停留在调用[queue inDatabase:]
等添加数据库操作的API上;在执行添加进来的操作时,是通过与serial
队列对应的单线程完成的,通过这种方式保证在执行操作时的数据同步与访问安全;
我们看一下FMDatabaseQueue
的初始化和添加block操作的内部实现:
//初始化方法
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
self = [super init];
if (self != nil) {
//初始化数据库对象
_db = [[[self class] databaseClass] databaseWithPath:aPath];
FMDBRetain(_db);
//根据SQLite的版本信息,调用不同的数据库打开方法
#if SQLITE_VERSION_NUMBER >= 3005000
BOOL success = [_db openWithFlags:openFlags vfs:vfsName];
#else
BOOL success = [_db open];
#endif
if (!success) {
NSLog(@"Could not create database queue for path %@", aPath);
FMDBRelease(self);
return 0x00;
}
_path = FMDBReturnRetained(aPath);
//生成执行数据库操作的serial队列
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
//设置这个queue的关联key/value标志信息,后面可以通过这个key在queue中获取出关联的self对象,用于判断是否会产生死锁
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
_vfsName = [vfsName copy];
}
return self;
}
//添加block操作到FMDatabaseQueue中的方法
- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {
#ifndef NDEBUG
/* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
* and then check it against self to make sure we're not about to deadlock. */
//根据key信息在当前执行queue中取出对象,大多数时候取出来的应该是nil才对,这里接着会判断是否会产生死锁
FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
#endif
FMDBRetain(self);
//在serial队列中同步执行传递进来的数据库操作block
dispatch_sync(_queue, ^() {
FMDatabase *db = [self database];
block(db); //执行block操作
if ([db hasOpenResultSets]) {
NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
//debug环境下打印一些查询的数据日志
#if defined(DEBUG) && DEBUG
NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
NSLog(@"query: '%@'", [rs query]);
}
#endif
}
});
FMDBRelease(self);
}
通过以上实现可以看到在初始化方法中生成了一个serial queue
队列,执行操作主要是通过serial queue
的dispatch_sync()
同步操作API完成的,这样就避免了访问数据时异步操作的可能性,从而保证多线程下的安全访问;
需要补充说明的是在初始化时通过调用dispatch_queue_set_specific ()
函数,设置了这个queue
的关联标识对象;然后在执行添加的block操作时,首先通过dispatch_get_specific
方法在queue
中取出标识对象,用来判断是否与当前的self对象相同,如果相同就意味着当前执行inDatabase
方法的队列与方法内的dispatch_sync(_queue, ...);
中的_queue
是同一个队列,在这种情况下会产生死锁,因此需要抛出程序异常;这里需要对GCD的API有一些基础理解,关于GCD的理解可以看一下这篇文章;
最后再看一个FMDatabaseQueue
的实际使用代码:
//新建FMDatabaseQueue对象
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
//通过inDatabase接口安全的执行数据库操作
[queue inDatabase:^(FMDatabase *adb) {
//在queue中执行create、insert等操作
[adb executeUpdate:@"create table qfoo (foo text)"];
[adb executeUpdate:@"insert into qfoo values ('hi')"];
[adb executeUpdate:@"insert into qfoo values ('hello')"];
[adb executeUpdate:@"insert into qfoo values ('not')"];
//执行查询操作
int count = 0;
FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"];
while ([rsl next]) {
count++;
}
}];