FMDB主要有以下几个类:
(1)FMDatabase:代表一个单独的SQLite操作实例,数据库通过它增删改查操作;
(2)FMResultSet:代表查询后的结果集;
(3)FMDatabaseQueue:代表串行队列,对多线程操作提供了支持;
(4)FMDatabaseAdditions:本类用于扩展FMDatabase,用于查找表是否存在,版本号等功能;
(5)FMDatabasePool:此方式官方是不推荐使用,代表是任务池,也是对多线程提供了支持。
.线程安全
在多个线程中同时使用一个FMDatabase实例是不明智的。不要让多个线程分享同一个FMDatabase实例,它无法在多个线程中同时使用。 如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱等问题。所以,请使用 FMDatabaseQueue,它是线程安全的。以下是使用方法:
1.创建
NSString *path = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES).firstObject;
NSString *filePath = [path stringByAppendingPathComponent:@"FMDB.db"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];
databaseQueueWithPath:方法里面创建了一个串行队列
2.操作数据
[queue inDatabase:^(FMDatabase*db) {
//FMDatabase数据库操作
if (![db open]) {
NSLog(@"打开数据库失败");
return ;
}
//创建表(FMDB中只有update和query操作,除了查询其他都是update操作)}];
[db executeUpdate:@"create table if not exists user(name text,gender text,age integer) "];
//插入数据
BOOL inser = [db executeUpdate:@"insert into user values(?,?,?)",_nameTextField.text,_sexTextField.text,_ageTextField.text];
[db close];
}
3.事务操作:
我们可以这样理解数据库事物:对数据库所做的一系列修改,在修改过程中,暂时不写入数据库,而是缓存起来,用户在自己的终端可以预览变化,直到全部修改完成,并经过检查确认无误后,一次性提交并写入数据库,在提交之前,必要的话所做的修改都可以取消。提交之后,就不能撤销,提交成功后其他用户才可以通过查询浏览数据的变化。
简单的说也就是,事务可以让多个表的数据同时插入,一旦有一个表操作失败,那么其他表也都会失败。当然这种说法是为了理解,不是严谨的。
那么对一个表大量插入数据时也可以用事务。比如sqlite3。
数据库 中 insert into 语句等操作是比较耗时的,假如我们一次性插入几百几千条数据就会造成主线程阻塞,以至于ui界面卡住。那么这时候我们就要开启一个事物来进行操作。
原因就是它以文件的形式存在磁盘中,每次访问时都要打开一次文件,如果对数据库进行大量的操作,就很慢。可是如果我们用事务的形式提交,开始事务后,进行的大量操作语句都保存在内存中,当提交commit时才全部写入数据库,此时,数据库文件也只用打开一次。如果操作错误,还可以回滚事务。
// 单线程事务
//事务
-(void)transaction
{
BOOL isSuccess=[_dataBase open];
if (!isSuccess) {
HSLog(@"打开数据库失败");
}
[_dataBase beginTransaction];
BOOL isRollBack = NO;
@try {
for (int i = 0; i<500; i++) {
NSString *nId = [NSString stringWithFormat:@"%d",i];
NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
BOOL a = [_dataBase executeUpdate:sql,nId,strName];
if (!a) {
NSLog(@"插入失败1");
}
}
}
@catch (NSException *exception) {
isRollBack = YES;
[_dataBase rollback];
}
@finally {
if (!isRollBack) {
[_dataBase commit];
}
}
[_dataBase close];
}
// 多线程事务
//事务
-(void)transaction
{
[_dataBase inTransaction:^(FMDatabase *db, BOOL *rollback) {
for (int i = 0; i<500; i++) {
NSString *nId = [NSString stringWithFormat:@"%d",i];
NSString *strName = [[NSString alloc] initWithFormat:@"student_%d",i];
NSString *sql = @"INSERT INTO Student (id,student_name) VALUES (?,?)";
BOOL a = [db executeUpdate:sql,nId,strName];
if (!a) {
*rollback = YES;
return;
}
}
}];
}
+ [FMDatabase databaseWithPath:]
// 核心其实还是调用了+[FMDataBase initWithPath:]函数,下面会详解
+ (instancetype)databaseWithPath:(NSString*)aPath {
// FMDBReturnAutoReleased是为了让FMDB兼容MRC和ARC,具体细节看下其宏定义就明白了
return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}
/** 初始化一个FMDataBase对象
根据path(aPath)来创建一个SQLite数据库。对应的aPath参数有三种情形:
1. 数据库文件路径:不为空字符串,不为nil。如果该文件路径不存在,那么SQLite会给你新建一个;
2. 空字符串@"":将在外存临时给你创建一个空的数据库,并且如果该数据库连接释放,那么对应数据库会自动删除;
3. nil:会在内存中创建数据库,随着该数据库连接的释放,也会释放该数据库;
- (instancetype)initWithPath:(NSString*)aPath {
// SQLite支持三种线程模式,sqlite3_threadsafe()函数的返回值可以确定编译时指定的线程模式。
// 三种模式分别为1.单线程模式 2.多线程模式 3.串行模式 其中对于单线程模式,sqlite3_threadsafe()返回false
// 对于另外两个模式,则返回true。这是因为单线程模式下没有进行互斥(mutex),所以多线程下是不安全的
assert(sqlite3_threadsafe());
self = [super init];
// 很多属性后面再提。不过这里值得注意的是_db居然赋值为nil,也就是说真正构建_db不是在initWithPath:这个函数中,这里透露下,其实作者是将构建部分代码放到了open函数中if (self) {
_databasePath = [aPath copy];
_openResultSets = [[NSMutableSet alloc] init];
_db = nil;
_logsErrors = YES;
_crashOnErrors = NO;
_maxBusyRetryTimeInterval = ;
}
return self;
}
+ [FMDatabase open]
上面提到过+ [FMDatabase databaseWithPath:]和- [FMDatabase initWithPath:]本质上只是给了数据库一个名字,并没有真实创建或者获取数据库。这里的open函数才是真正获取到数据库,其本质上也就是调用SQLite的C/C++接口 – sqlite3_open()。
sqlite3_open(const char *filename, sqlite3 **ppDb)
该例子打开一个指向 SQLite 数据库文件的连接,返回一个用于其他 SQLite 程序的数据库连接对象。
如果 filename 参数是 NULL 或 ':memory:',那么 sqlite3_open() 将会在 RAM 中创建一个内存数据库,这只会在 session 的有效时间内持续。
如果文件名 filename 不为 NULL,那么 sqlite3_open() 将使用这个参数值尝试打开数据库文件。如果该名称的文件不存在,sqlite3_open() 将创建一个新的命名为该名称的数据库文件并打开。
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
// 若_maxBusyRetryTimeInterval大于0,那么就调用setMaxBusyRetryTimeInterval:函数
// setMaxBusyRetryTimeInterval:函数主要是调用sqlite3_busy_handler来处理其他线程已经在操作数据库的情况,默认_maxBusyRetryTimeInterval为2。
// 具体该参数有什么用,下面在FMDBDatabaseBusyHandler函数中会详解。
if (_maxBusyRetryTimeInterval > 0.0) {
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
return YES;
}
- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
_maxBusyRetryTimeInterval = timeout;
if (!_db) {
return;
}
// 处理的handler设置为FMDBDatabaseBusyHandler这个函数
if (timeout > ) {
sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
}
else {
// 不使用任何busy handler处理
sqlite3_busy_handler(_db, nil, nil);
}
}
这里需要提一下sqlite3_busy_handler这个函数:
int sqlite3_busy_handler(sqlite3, int()(void,int), void);
第一个参数是告知哪个数据库需要设置busy handler。
第二个参数是其实就是回调函数(busy handler)了,当你调用该回调函数时,需传递给它的一个void*的参数的拷贝,也即sqlite3_busy_handler的第三个参数;另一个需要传给回调函数的int参数是表示这次锁事件,该回调函数被调用的次数。如果回调函数返回0时,将不再尝试再次访问数据库而返回SQLITE_BUSY或者SQLITE_IOERR_BLOCKED。如果回调函数返回非0, 将会不断尝试操作数据库。
总结:程序运行过程中,如果有其他进程或者线程在读写数据库,那么sqlite3_busy_handler会不断调用回调函数,直到其他进程或者线程释放锁。获得锁之后,不会再调用回调函数,从而向下执行,进行数据库操作。该函数是在获取不到锁的时候,以执行回调函数的次数来进行延迟,等待其他进程或者线程操作数据库结束,从而获得锁操作数据库。
// 注意:appledoc(生成文档的软件)中,对于有具体实现的C函数,比如下面这个函数,
// 是有bug的。所以你在生成文档时,忽略.m文件。
// 该函数就是简单调用sqlite3_sleep来挂起进程
static int FMDBDatabaseBusyHandler(void *f, int count) {
FMDatabase *self = (__bridge FMDatabase*)f;
// 如果count为0,表示的第一次执行回调函数
// 初始化self->_startBusyRetryTime,供后面计算delta使用
if (count == ) {
self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
return ;
}
// 使用delta变量控制执行回调函数的次数,每次挂起50~100ms
// 所以maxBusyRetryTimeInterval的作用就在这体现出来了
// 当挂起的时长大于maxBusyRetryTimeInterval,就返回0,并停止执行该回调函数了
NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
if (delta < [self maxBusyRetryTimeInterval]) {
// 使用sqlite3_sleep每次当前线程挂起50~100ms
int requestedSleepInMillseconds = (int) arc4random_uniform() + ;
int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds);
// 如果实际挂起的时长与想要挂起的时长不一致,可能是因为构建SQLite时没将HAVE_USLEEP置为1
if (actualSleepInMilliseconds != requestedSleepInMillseconds) {
NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds);
}
return ;
}
return ;
}
[FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:](重点)
[FMDatabase executeQuery:]等等类似的函数,最终都是对- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]的简单封装。该函数比较关键,主要是针对查询的sql语句。
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
// 判断当前是否存在数据库以供操作
if (![self databaseExists]) {
return 0x00;
}
// 如果当前线程已经在使用数据库了,那就输出正在使用的警告
if (_isExecutingStatement) {
[self warnInUse];
return 0x00;
}
_isExecutingStatement = YES;
int rc = 0x00;
sqlite3_stmt *pStmt = 0x00; // sqlite的prepared语句类型
FMStatement *statement = 0x00; // 对sqlite3_stmt的简单封装,在实际应用中,你不应直接操作FMStatement对象
FMResultSet *rs = 0x00; // FMResultSet对象是用来获取最终查询结果的
// 需要追踪sql执行状态的话,输出执行状态
if (_traceExecution && sql) {
NSLog(@"%@ executeQuery: %@", self, sql);
}
// 调用sql语句之前,首先要将sql字符串预处理一下,转化为SQLite可用的prepared语句(预处理语句)
// 使用sqlite3_prepare_v2来生成sql对应的prepare语句(即pStmt)代价很大
// 所以建议使用缓存机制来减少对sqlite3_prepare_v2的使用
if (_shouldCacheStatements) {
// 获取到缓存中的prepared语句
statement = [self cachedStatementForQuery:sql];
pStmt = statement ? [statement statement] : 0x00;
// prepared语句可以被重置(调用sqlite3_reset函数),然后可以重新绑定参数以便重新执行。
[statement reset];
}
// 如果缓存中没有sql对应的prepared语句,那么只能使用sqlite3_prepare_v2函数进行预处理
if (!pStmt) {
rc = sqlite3_prepare_v2(_db, [sql UTF8String], -, &pStmt, );
// 如果生成prepared语句出错,那么就根据是否需要打印错误信息(_logsErrors)以及是否遇到错误直接中止程序执行(_crashOnErrors)来执行出错处理。
// 最后调用sqlite3_finalize函数释放所有的内部资源和sqlite3_stmt数据结构,有效删除prepared语句。
if (SQLITE_OK != rc) {
if (_logsErrors) {
NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
NSLog(@"DB Query: %@", sql);
NSLog(@"DB Path: %@", _databasePath);
}
if (_crashOnErrors) {
NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
// abort()函数表示中止程序执行,直接从调用的地方跳出。
abort();
}
sqlite3_finalize(pStmt);
_isExecutingStatement = NO;
return nil;
}
}
id obj;
int idx = ;
// 获取到pStmt中需要绑定的参数个数
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
if (dictionaryArgs) {
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
// 在每个dictionaryKey之前加上冒号,比如上面的a -> :a,方便获取参数在prepared语句中的索引
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
// 查看执行状况
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
// 在prepared语句中查找对应parameterName的参数索引值namedIdx
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
FMDBRelease(parameterName);
// 可以利用索引namedIdx获取对应参数,再使用bindObject:函数将dictionaryArgs保存的value绑定给对应参数
if (namedIdx > ) {
[self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
// 使用这个idx来判断sql中的所有参数值是否都绑定上了
idx++;
}
else {
NSLog(@"Could not find index for %@", dictionaryKey);
}
}
}
else {
while (idx < queryCount) {
// 使用arrayArgs的例子
/**
[db executeQuery:@"insert into testOneHundredTwelvePointTwo values (?, ?)" withArgumentsInArray:[NSArray arrayWithObjects:@"one", [NSNumber numberWithInteger:2], nil]];
*/
if (arrayArgs && idx < (int)[arrayArgs count]) {
obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
}
// 使用args的例子,使用args其实就是调用- (FMResultSet *)executeQuery:(NSString*)sql, ...;
/**
FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];
*/
else if (args) {
obj = va_arg(args, id);
}
else {
break;
}
if (_traceExecution) {
if ([obj isKindOfClass:[NSData class]]) {
NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]);
}
else {
NSLog(@"obj: %@", obj);
}
}
idx++;
// 绑定参数值
[self bindObject:obj toColumn:idx inStatement:pStmt];
}
}
// 如果绑定的参数数目不对,认为出错,并释放资源
if (idx != queryCount) {
NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
sqlite3_finalize(pStmt);
_isExecutingStatement = NO;
return nil;
}
FMDBRetain(statement); // to balance the release below
// statement不为空,进行缓存
if (!statement) {
statement = [[FMStatement alloc] init];
[statement setStatement:pStmt];
// 使用sql作为key来缓存statement(即sql对应的prepare语句)
if (_shouldCacheStatements && sql) {
[self setCachedStatement:statement forQuery:sql];
}
}
// 根据statement和self(FMDatabase对象)构建一个FMResultSet对象,此函数中仅仅是构建该对象,还没使用next等函数获取查询结果
// 注意FMResultSet中含有以下成员(除了最后一个,其他成员均在此处初始化过了)
/**
@interface FMResultSet : NSObject {
FMDatabase *_parentDB; // 表示该对象查询的数据库,主要是为了能在FMResultSet自己的函数中索引到正在操作的FMDatabase对象
FMStatement *_statement; // prepared语句
NSString *_query; // 对应的sql查询语句
NSMutableDictionary *_columnNameToIndexMap;
}
*/
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
// 将此时的FMResultSet对象添加_openResultSets,主要是为了调试
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet];
// 并设置statement的使用数目useCount加1,暂时不清楚此成员有何作用,感觉也是用于调试
[statement setUseCount:[statement useCount] + ];
FMDBRelease(statement);
// 生成statement的操作已经结束
_isExecutingStatement = NO;
return rs;
}
举例dictionaryArgs: NSMutableDictionary
*dictionaryArgs = [NSMutableDictionary dictionary]; [dictionaryArgs setObject:@"Text1" forKey:@"a"]; [db executeQuery:@"select * from namedparamcounttest where a = :a" withParameterDictionary:dictionaryArgs]; // 注意类似:AAA前面有冒号的就是参数 // 其他的参数形式如:"?", "?NNN", ":AAA", "$AAA", 或 "@AAA" */
[FMResultSet nextWithError:]
- [FMResultSet next]函数其实就是对nextWithError:的简单封装。作用就是从我们上一步open中获取到的FMResultSet对象中读取查询后结果的每一行,交给用户自己处理。读取每一行的方法(即next)其实就是封装了sqlite3_step函数。而nextWithError:主要封装了对sqlite3_step函数返回结果的处理。
int sqlite3_step(sqlite3_stmt*);
sqlite3_prepare函数将SQL命令字符串解析并转换为一系列的命令字节码,这些字节码最终被传送到SQlite3的虚拟数据库引擎(VDBE: Virtual Database Engine)中执行,完成这项工作的是sqlite3_step函数。比如一个SELECT查询操作,sqlite3_step函数的每次调用都会返回结果集中的其中一行,直到再没有有效数据行了。每次调用sqlite3_step函数如果返回SQLITE_ROW,代表获得了有效数据行,可以通过sqlite3_column函数提取某列的值。如果调用sqlite3_step函数返回SQLITE_DONE,则代表prepared语句已经执行到终点了,没有有效数据了。很多命令第一次调用sqlite3_step函数就会返回SQLITE_DONE,因为这些SQL命令不会返回数据。对于INSERT,UPDATE,DELETE命令,会返回它们所修改的行号——一个单行单列的值。
// 返回YES表示从数据库中获取到了下一行数据
- (BOOL)nextWithError:(NSError **)outErr {
// 尝试步进到下一行
int rc = sqlite3_step([_statement statement]);
// 对返回结果rc进行处理
/**
SQLITE_BUSY 数据库文件有锁
SQLITE_LOCKED 数据库中的某张表有锁
SQLITE_DONE sqlite3_step()执行完毕
SQLITE_ROW sqlite3_step()获取到下一行数据
SQLITE_ERROR 一般用于没有特别指定错误码的错误,就是说函数在执行过程中发生了错误,但无法知道错误发生的原因。
SQLITE_MISUSE 没有正确使用SQLite接口,比如一条语句在sqlite3_step函数执行之后,没有被重置之前,再次给其绑定参数,这时bind函数就会返回SQLITE_MISUSE。
*/
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
NSLog(@"Database busy");
if (outErr) {
// lastError使用sqlite3_errcode获取到错误码,封装成NSError对象返回
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
// all is well, let's return.
}
else if (SQLITE_ERROR == rc) {
// sqliteHandle就是获取到对应FMDatabase对象,然后使用sqlite3_errmsg来获取错误码的字符串
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_MISUSE == rc) {
// uh oh.
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
if (_parentDB) {
*outErr = [_parentDB lastError];
}
else {
// 如果next和nextWithError函数是在当前的FMResultSet关闭之后调用的
// 这时输出的错误信息应该是parentDB不存在
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
*outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
}
}
}
else {
// wtf?
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
// 如果不是读取下一行数据,那么就关闭数据库
if (rc != SQLITE_ROW) {
[self close];
}
return (rc == SQLITE_ROW);
}
[FMDatabase close]
与open函数成对调用。主要还是封装了sqlite_close函数。
- (BOOL)close {
// 清除缓存的prepared语句,下面会详解
[self clearCachedStatements];
// 关闭所有打开的FMResultSet对象,目前看来这个_openResultSets大概也是用来调试的
[self closeOpenResultSets];
if (!_db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
do {
retry = NO;
// 调用sqlite3_close来尝试关闭数据库
rc = sqlite3_close(_db);
//如果当前数据库上锁,那么就先尝试重新关闭(置retry为YES) // 同时还尝试释放数据库中的prepared语句资源
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
// sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt)表示从数据库pDb中对应的pStmt语句开始一个个往下找出相应prepared语句,如果pStmt为nil,那么就从pDb的第一个prepared语句开始。
// 此处迭代找到数据库中所有prepared语句,释放其资源。
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
}
// 关闭出错,输出错误码
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
_db = nil;
return YES;
}
// _cachedStatements是用来缓存prepared语句的,所以清空_cachedStatements就是将每个缓存的prepared语句释放
// 具体实现就是使用下面那个close函数,close函数中调用了sqlite_finalize函数释放资源
- (void)clearCachedStatements {
for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
// makeObjectsPerformSelector会并发执行同一件事,所以效率比for循环一个个执行要快很多
[statements makeObjectsPerformSelector:@selector(close)];
}
[_cachedStatements removeAllObjects];
}
// 注意:此为FMResultSet的close函数
- (void)close {
if (_statement) {
sqlite3_finalize(_statement);
_statement = 0x00;
}
_inUse = NO;
}
// 清除_openResultSets
- (void)closeOpenResultSets {
//Copy the set so we don't get mutation errors
NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]);
// 迭代关闭_openResultSets中的FMResultSet对象
for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
// 清除FMResultSet的操作
[rs setParentDB:nil];
[rs close];
[_openResultSets removeObject:rsInWrappedInATastyValueMeal];
}
}
executeUpdate:系列函数
注意除了“SELECT”语句外,其他的SQL语句都需要使用executeUpdate:系列函数,这些SQL语句包括CREATE
, UPDATE
, INSERT
, ALTER
, COMMIT
, BEGIN
, DETACH
, DELETE
, DROP
, END
, EXPLAIN
, VACUUM
, 和REPLACE
等等。
基本上所有executeUpdate:系列函数都是对- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的封装。注意- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]函数的具体实现,基本和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]大部分实现是差不多的,关键在于executeQuery是查询语句,所以它需要FMResultSet来保存查询的结果。而executeUpdate是非查询语句,不需要保存查询结果,但需要调用sqlite3_step(pStmt)来执行该SQL语句。这里就不赘述了,详见源码。
executeStatements:系列函数
使用executeStatements:函数可以将多个SQL执行语句写在一个字符串中,并执行。具体使用举例如下:
NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
"create table bulktest2 (id integer primary key autoincrement, y text);"
"create table bulktest3 (id integer primary key autoincrement, z text);"
"insert into bulktest1 (x) values ('XXX');"
"insert into bulktest2 (y) values ('YYY');"
"insert into bulktest3 (z) values ('ZZZ');";
success = [db executeStatements:sql];
sql = @"select count(*) as count from bulktest1;"
"select count(*) as count from bulktest2;"
"select count(*) as count from bulktest3;";
success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
NSInteger count = [dictionary[@"count"] integerValue];
XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary);
return 0;
}];
基本上executeStatements:系列函数最终封装的都是- [FMDatabase executeStatements:withResultBlock:]函数,而此函数又是对sqlite3_exec函数的封装。
sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)
该例程提供了一个执行 SQL 命令的快捷方式,SQL 命令由 sql 参数提供,可以由多个 SQL 命令组成。
在这里,第一个参数 sqlite3 是打开的数据库对象,sqlite_callback 是一个回调,data 作为其第一个参数,errmsg 将被返回用来获取程序生成的任何错误。
sqlite3_exec() 程序解析并执行由 sql 参数所给的每个命令,直到字符串结束或者遇到错误为止。
executeStatements:源码如下:
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block {
int rc;
char *errmsg = nil;
rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);
if (errmsg && [self logsErrors]) {
NSLog(@"Error inserting batch: %s", errmsg);
sqlite3_free(errmsg);
}
return (rc == SQLITE_OK);
}
executeQueryWithFormat:和executeUpdateWithFormat:函数
考虑到如果用户直接调用printf那种形式的字符串(比如“ INSERT INTO myTable (%@) VALUES (%d)”, “age”,25),那么就需要自己将对应字符串处理成相应的SQL语句。恰好executeQuery和executeUpdate系列函数提供了相应的接口:
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
其实这两个函数和其他executeQuery和executeUpdate系列方法,多的就是一个将format和…转化为可用的SQL语句步骤。其它部分其实本质还是调用- [FMDatabase executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:]和- [FMDatabase executeQuery:withArgumentsInArray:orDictionary:orVAList:]。下面仅列出format和…的转化代码:
va_list args;
// 将args指向format中第一个参数
va_start(args, format);
NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];
// 使用extractSQL函数将format和args转化为sql和arguments供后面函数使用
[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
// 关闭args,与va_start成对出现
va_end(args);
至于extractSQL:这个函数其实就是将(“INSERT INTO myTable (%@) VALUES (%d)”, “age”,25)中的%s和%d这种符号变成”?”,然后将”age”和25加入到arguments中。具体实现如下:
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments {
NSUInteger length = [sql length];
unichar last = '\0';
for (NSUInteger i = 0; i < length; ++i) {
id arg = nil;
/** 使用last和current两个变量(有些还需要next变量,比如%llu)判断当前扫描到的字符串是不是%@、
%c、%s、%d等等。举个例子,如果碰到%s,那么说明我替换的参数其实是一个字符串,所以使用arg =
[NSString stringWithUTF8String:]获取到相应的arg作为参数值,
*/
// 注意type va_arg(va_list arg_ptr,type)函数是根据传入的type参数决定返回值类型的
// 另外它的作用是获取下一个参数的地址
unichar current = [sql characterAtIndex:i];
unichar add = current;
if (last == '%') {
switch (current) {
case '@':
arg = va_arg(args, id);
break;
case 'c':
// warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSString stringWithFormat:@"%c", va_arg(args, int)];
break;
case 's':
arg = [NSString stringWithUTF8String:va_arg(args, char*)];
break;
case 'd':
case 'D':
case 'i':
arg = [NSNumber numberWithInt:va_arg(args, int)];
break;
case 'u':
case 'U':
arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)];
break;
// %hi表示short int,%hu表示short unsigned int
case 'h':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
// warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithShort:(short)(va_arg(args, int))];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
// warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int'
arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))];
}
else {
i--;
}
break;
// %qi表示long long,%qu表示unsigned long long
case 'q':
i++;
if (i < length && [sql characterAtIndex:i] == 'i') {
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
break;
case 'f':
arg = [NSNumber numberWithDouble:va_arg(args, double)];
break;
// %g原本是根据数据选择合适的方式输出(浮点数还是科学计数法),不过此处是用float类型输出
case 'g':
// warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'
arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))];
break;
case 'l':
i++;
if (i < length) {
unichar next = [sql characterAtIndex:i];
if (next == 'l') {
i++;
if (i < length && [sql characterAtIndex:i] == 'd') {
//%lld
arg = [NSNumber numberWithLongLong:va_arg(args, long long)];
}
else if (i < length && [sql characterAtIndex:i] == 'u') {
//%llu
arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)];
}
else {
i--;
}
}
else if (next == 'd') {
//%ld
arg = [NSNumber numberWithLong:va_arg(args, long)];
}
else if (next == 'u') {
//%lu
arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)];
}
else {
i--;
}
}
else {
i--;
}
break;
default:
// something else that we can't interpret. just pass it on through like normal
break;
}
}
else if (current == '%') {
// 遇到%,直接跳过。
add = '\0';
}
// 如果arg不为空,表示确定arg是参数,那么就使用?替换它,并将其对应参数值arg添加到arguments
if (arg != nil) {
[cleanedSQL appendString:@"?"];
[arguments addObject:arg];
}
// 如果参数格式是%@,但此时arg是空,那么就替换为NULL
else if (add == (unichar)'@' && last == (unichar) '%') {
[cleanedSQL appendFormat:@"NULL"];
}
// 如果不是参数,就用原先字符串替换
else if (add != '\0') {
[cleanedSQL appendFormat:@"%C", add];
}
last = current;
}
}
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt
该函数是用来在pStmt中绑定参数值到指定(根据idx)参数上。具体封装的是sqlite3_bind系列函数。
如果要使用sqlite3_bind系列函数,需要指定三个参数,一个是正在使用的sqlite_stmt对象,一个是参数索引idx,还有一个就是需要绑定的参数值,此函数解决的关键就是根据obj判断出其类型,然后调用相关的sqlite3_bind*函数,比如obj是int型,那么就调用sqlite3_bind_int函数。又或者obj是NSData类型,那么就调用sqlite_bind_blob函数。具体后面详细解释。
- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
// 如果obj为指针为空,那么就使用sqlite3_bind_null给该参数绑定SQL null。
if ((!obj) || ((NSNull *)obj == [NSNull null])) {
sqlite3_bind_null(pStmt, idx);
}
// FIXME - someday check the return codes on these binds.
else if ([obj isKindOfClass:[NSData class]]) {
const void *bytes = [obj bytes];
if (!bytes) {
// 如果obj是一个空的NSData对象
// 不要直接将NULL指针作为参数值,否则sqlite会绑定一个NULL指针给参数,而不是一个blob对象(Binary Large Object)
bytes = "";
}
// SQLITE_STATIC表示传过来参数值的指针是不变的,所以完事后不需要销毁它,与其相对的是
SQLITE_TRANSIENT
sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
}
// 如果obj是一个NSDate对象
else if ([obj isKindOfClass:[NSDate class]]) {
// 如果你自定义了Date格式,那么就将该NSDate转化为你定义的格式,并绑定到参数上
// 如果没有自定义Date格式,那么默认使用timeIntervalSince1970来计算参数值进行绑定
if (self.hasDateFormatter)
sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
else
sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
}
// 如果是NSNumber对象,注意此处判断obj类型的方法
// @encode,@编译器指令之一,返回一个给定类型编码为一种内部表示的字符串(例如,@encode(int) → i),类似于 ANSI C 的 typeof 操作。苹果的 Objective-C 运行时库内部利用类型编码帮助加快消息分发。
else if ([obj isKindOfClass:[NSNumber class]]) {
if (strcmp([obj objCType], @encode(char)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj charValue]);
}
else if (strcmp([obj objCType], @encode(unsigned char)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]);
}
else if (strcmp([obj objCType], @encode(short)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj shortValue]);
}
else if (strcmp([obj objCType], @encode(unsigned short)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]);
}
else if (strcmp([obj objCType], @encode(int)) == 0) {
sqlite3_bind_int(pStmt, idx, [obj intValue]);
}
else if (strcmp([obj objCType], @encode(unsigned int)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]);
}
else if (strcmp([obj objCType], @encode(long)) == 0) {
sqlite3_bind_int64(pStmt, idx, [obj longValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]);
}
else if (strcmp([obj objCType], @encode(long long)) == 0) {
sqlite3_bind_int64(pStmt, idx, [obj longLongValue]);
}
else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) {
sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]);
}
else if (strcmp([obj objCType], @encode(float)) == 0) {
sqlite3_bind_double(pStmt, idx, [obj floatValue]);
}
else if (strcmp([obj objCType], @encode(double)) == 0) {
sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
}
else if (strcmp([obj objCType], @encode(BOOL)) == 0) { // bool使用sqlite3_bind_int来绑定的
sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
}
else {
sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
}
}
else {
sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
}
}
openWithFlags:系列函数
除了前面提到过的open函数外,FMDB还为我们提供了openWithFlags:系列函数,其本质是封装了sqlite3_open_v2。
int sqlite3_open_v2(
const char *filename, /* 数据库名称 (UTF-8) */
sqlite3 **ppDb, /* 输出: SQLite数据库对象 */
int flags, /* 标识符 */
const char *zVfs /* 想要使用的VFS名称 */
)
对于sqlite3_open和sqlite3_open16函数,如果可能将以可读可写的方式打开数据库,否则以只读的方式打开数据库。如果要打开的数据库文件不存在,就新建一个。对于sqlite3_open_v2函数,情况就要复杂一些了,因为这个v2版本的函数强大就强大在它可以对打开(连接)数据库的方式进行控制,具体是通过它的参数flags来完成。sqlite3_open_v2函数只支持UTF-8编码的SQlite3数据库文件。
如flags设置为SQLITE_OPEN_READONLY,则SQlite3数据库文件以只读的方式打开,如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件本身被操作系统设置为写保护状态,则以只读的方式打开。如果该数据库文件不存在,则sqlite3_open_v2函数执行失败,返回一个error。如果flags设置为SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE,则SQlite3数据库文件以可读可写的方式打开,如果该数据库文件不存在则新建一个。这也是sqlite3_open和sqlite3_open16函数的默认行为。除此之外,flags还可以设置为其他标志,具体可以查看SQlite官方文档。
参数zVfs允许客户应用程序命名一个虚拟文件系统(Virtual File System)模块,用来与数据库连接。VFS作为SQlite library和底层存储系统(如某个文件系统)之间的一个抽象层,通常客户应用程序可以简单的给该参数传递一个NULL指针,以使用默认的VFS模块。
对于UTF-8编码的SQlite3数据库文件,推荐使用sqlite3_open_v2函数进行连接,它可以对数据库文件的打开和处理操作进行更多的控制。
FMResultSet其他的获取结果方式
FMResultSet的resultSetWithStatement:、close、next函数。其实FMResultSet除了使用next获取查询结果外,还有很多其他的接口可以查询到结果。
一系列的ForColumn:和ForColumnIndex:(表示对应的数据类型)函数都是用来获取查询结果的。这里值得注意的是ForColumn:函数本质是调用相应的*ForColumnIndex:函数。比如:
- (int)intForColumn:(NSString*)columnName {
return [self intForColumnIndex:[self columnIndexForName:columnName]];
}
上述函数实现内部做了一个转化,就是利用columIndexForName:函数查询到这个columnName对应的索引值。而这个columnIndexForName:本质是根据_columnNameToIndexMap属性获取到列名称(columnName)的对应列号(columnIdx)。_columnNameToIndexMap是一个NSMutableDictionary对象。其中key表示的是指定结果集中对应列的名称,value表示的是指定结果集中对应的列号(columnIdx)。所以我们这里主要看下columnNameToIndexMap的实现:
- (NSMutableDictionary *)columnNameToIndexMap {
if (!_columnNameToIndexMap) {
// 找出由statement指定的结果集中列的数目
int columnCount = sqlite3_column_count([_statement statement]);
_columnNameToIndexMap = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
int columnIdx = 0;
// 将列号和该列对应名称绑定在一起,组成_columnNameToIndexMap
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
[_columnNameToIndexMap setObject:[NSNumber numberWithInt:columnIdx]
forKey:[[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)] lowercaseString]];
}
}
return _columnNameToIndexMap;
}
这时我们再回头看看*ForColumnIndex:函数的实现。它的本质就是调用sqlite3_column_*(*表示对应的数据类型),也就是从statement中获取到对应列号的数据,比如
- (int)intForColumnIndex:(int)columnIdx {
return sqlite3_column_int([_statement statement], columnIdx);
}
FMDB的加解密
FMDB中使用- [FMDatabase setKey:]和- [FMDatabase setKeyWithData:]输入数据库密码以求验证用户身份,使用- [FMDatabase rekey:]和- [FMDatabase rekeyWithData:]来给数据库设置密码或者清除密码。这两类函数分别对sqlite3_key和sqlite3_rekey函数进行了封装。
int sqlite3_key( sqlite3 *db, const void *pKey, int nKey)
db 是指定数据库,pKey 是密钥,nKey 是密钥长度。例:sqlite3_key( db, "abc", 3);
sqlite3_key是输入密钥,如果数据库已加密必须先执行此函数并输入正确密钥才能进行操作,如果数据库没有加密,执行此函数后进行数据库操作反而会出现“此数据库已加密或不是一个数据库文件”的错误。
int sqlite3_rekey( sqlite3 *db, const void *pKey, int nKey)
参数同sqlite3_key。
sqlite3_rekey是变更密钥或给没有加密的数据库添加密钥或清空密钥,变更密钥或清空密钥前必须先正确执行 sqlite3_key。在正确执行 sqlite3_rekey 之后在 sqlite3_close 关闭数据库之前可以正常操作数据库,不需要再执行 sqlite3_key。
清空密钥为 sqlite3_rekey( db, NULL, 0)。
FMDatabaseQueue使用举例
// 创建,最好放在一个单例的类中
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
// 使用
[queue
inDatabase
:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
// …
}
}];
// 如果要支持事务
[queue
inTransaction
:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// etc…
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:]];
}];
我们可以看到FMDB的多线程实现主要是依赖于FMDatabaseQueue这个类。
+ [FMDatabaseQueue databaseQueueWithPath:]
// 调用initWithPath:函数构建一个FMDatabaseQueue对象
+ (instancetype)databaseQueueWithPath:(NSString*)aPath {
FMDatabaseQueue *q = [[self alloc] initWithPath:aPath];
FMDBAutorelease(q);
return q;
}
// 使用aPath作为数据库名称,并传入openFlags和vfsName作为openWithFlags:vfs:函数的参数
// 初始化一个database和相应的queue
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
// 除了另外定义了一个_queue外,其他部分和FMDatabase的初始化没什么不同
self = [super init];
if (self != nil) {
_db = [[[self class] databaseClass] databaseWithPath:aPath];
FMDBRetain(_db);
#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);
// 创建了一个串行队列
_queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
/** 给_queue这个GCD队列指定了一个kDispatchQueueSpecificKey字符串,并和self(即当前FMDatabaseQueue对象)进行绑定。日后可以通过此字符串获取到绑定的对象(此处就是self)。当然,你要保证正在执行的GCD队列是你之前指定的那个_queue队列。是不是有objc_setAssociatedObject函数的感觉。
此步骤的作用后面inDatabase函数中会具体讲解。
*/
dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
_openFlags = openFlags;
}
return self;
}
[FMDatabaseQueue inDatabase:]
注意inDatabase的参数是一个block。这个block一般是封装了数据库的操作,另外这个block在inDatabase中是同步执行的。
- (void)inDatabase:(void (^)(FMDatabase *db))block {
/* 使用dispatch_get_specific来查看当前queue是否是之前设定的那个_queue,如果是的话,那么使用kDispatchQueueSpecificKey作为参数传给dispatch_get_specific,如果返回的值不为空,那么返回值应该就是上面initWithPath:函数中绑定的那个FMDatabaseQueue对象。有人说除了当前queue还有可能有其他什么queue?这就是FMDatabaseQueue的用途,你可以创建多个FMDatabaseQueue对象来并发执行不同的SQL语句。
另外为啥要判断是不是当前执行的这个queue?是为了防止死锁!
*/
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");
FMDBRetain(self);
// 在当前这个queue中同步执行block
dispatch_sync(_queue, ^() {
FMDatabase *db = [self database];
block(db);
// 下面这部分你也看到了,定义了DEBUG宏,明显是用来调试用的。就不赘述了
if ([db hasOpenResultSets]) {
NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
#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);
}
其实我们从这个函数中就可以看出FMDatabaseQueue具体是怎么完成多线程的:
[FMDatabaseQueue inTransaction:]
该函数主要是针对数据库事务的处理:
- (void)inTransaction:(void (^)(FMDatabase *db, BOOL *rollback))block {
[self beginTransaction:NO withBlock:block];
}
可以看到,内部直接封装的是beginTransaction:withBlock:函数,那我们直接来看beginTransaction:withBlock:函数。
- (void)beginTransaction:(BOOL)useDeferred withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
FMDBRetain(self);
dispatch_sync(_queue, ^() {
BOOL shouldRollback = NO;
if (useDeferred) {
// 如果使用延迟事务,那么就调用该函数,下面有对该函数的详解
// 想令useDeferred为YES,可以调用与inTransaction相对的inDeferredTransaction函数
[[self database] beginDeferredTransaction];
}
else {
// 默认使用排他事务,下面有排他事务的详解
[[self database] beginTransaction];
}
// 注意该block除了要创建相应的数据库事务,还需要根据需要选择是否需要回滚
// 比如上面如果数据库操作出错了,那么你可以设置需要回滚,即返回shouldRollback为YES
block([self database], &shouldRollback);
// 如果需要回滚,那么就调用FMDatabase的rollback函数
if (shouldRollback) {
[[self database] rollback];
}
// 如果不需要回滚,那么就调用FMDatabase的commit函数确认提交相应SQL操作
else {
[[self database] commit];
}
});
FMDBRelease(self);
}
// 通过执行rollback transaction语句来执行回滚操作
- (BOOL)rollback {
BOOL b = [self executeUpdate:@"rollback transaction"];
// 既然已经回滚了,那么表示是否在进行事务的_inTransaction属性也要置为NO
if (b) {
_inTransaction = NO;
}
return b;
}
// 通过执行commit transaction语句来执行提交事务操作
- (BOOL)commit {
BOOL b = [self executeUpdate:@"commit transaction"];
// 既然已经提交过事务了,那么表示是否在进行事务的_inTransaction属性也要置为NO
if (b) {
_inTransaction = NO;
}
return b;
}
// 延迟事务指的是在对数据库操作前不进行任何加锁。默认情况下,
// 如果仅仅用BEGIN开始一个事务,那么事务就是DEFERRED的,同时它不会获取任何锁
- (BOOL)beginDeferredTransaction {
BOOL b = [self executeUpdate:@"begin deferred transaction"];
if (b) {
_inTransaction = YES;
}
return b;
}
// 默认进行的是排他(exclusive)操作
// 排他操作的实质是在开始对数据库读写前,获得EXCLUSIVE锁,即排他锁。排它锁说白点就是
// 告诉数据库别的连接:这是我独有的,谁都别想占有。
- (BOOL)beginTransaction {
BOOL b = [self executeUpdate:@"begin exclusive transaction"];
if (b) {
_inTransaction = YES;
}
return b;
}
[FMDatabaseQueue inSavePoint:]
savepoint类似于游戏存档一样的东西,一般的rollback相当于游戏重新开始,而加了savepoint后,相当于回到存档的位置然后接着游戏。与inDatabase和inTransaction相对有一个inSavePoint:的方法(相当于加了save point功能的inDatabase函数)。
/*
save point功能只在SQLite3.7及以上版本中使用,所以下面多数代码加上了
#if SQLITE_VERSION_NUMBER >= 3007000
#else
#endif
*/
- (NSError*)inSavePoint:(void (^)(FMDatabase *db, BOOL *rollback))block {
#if SQLITE_VERSION_NUMBER >= 3007000
static unsigned long savePointIdx = ;
__block NSError *err = 0x00;
FMDBRetain(self);
// 同步执行
dispatch_sync(_queue, ^() {
// 设定savepoint的名称,即给游戏存档设一个名字
NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
// 默认不回滚
BOOL shouldRollback = NO;
// 在执行block之前,先进行存档(save point)。如果有问题,直接退回这个存档(save point)
if ([[self database] startSavePointWithName:name error:&err]) {
block([self database], &shouldRollback);
// 如果需要回滚,调用rollbackToSavePointWithName:error:回滚到存档位置(savepoint)
if (shouldRollback) {
[[self database] rollbackToSavePointWithName:name error:&err];
}
// 记得执行完block后,不管有没有回滚,还需要释放掉这个存档
[[self database] releaseSavePointWithName:name error:&err];
}
});
FMDBRelease(self);
return err;
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return [NSError errorWithDomain:@"FMDatabase" code: userInfo:@{NSLocalizedDescriptionKey : errorMessage}];
#endif
}
// 调用savepoint $savepointname的SQL语句对数据库操作进行存档
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 使用release savepoint $savepointname的SQL语句删除存档,主要是为了释放资源
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
// 调用rollback transaction to savepoint $savepointname的SQL语句来回退到存档处
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr {
#if SQLITE_VERSION_NUMBER >= 3007000
NSParameterAssert(name);
NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)];
return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
#else
NSString *errorMessage = NSLocalizedString(@"Save point functions require SQLite 3.7", nil);
if (self.logsErrors) NSLog(@"%@", errorMessage);
return NO;
#endif
}
总结
FMDB比较常用的几个类基本上学习完毕。FMDB代码上不是很难,核心还是SQLite3和数据库的知识。更重要的还是要知道真实环境中的最佳实践。