前言
在上篇文章中介绍了文件的组成并详细的介绍了 FMResultSet
类,本文将接着上篇的分析进行 FMDatabase
文件的解读。
文件组成
FMDB源码主要有一下几个文件组成:
FMDatabase:
表示一个单独的SQLite DB实例,通过它可以对数据库进行增删改查等操作。
FMResultSet:
表示通过sql在DB中查询到的结果集,并且将查询结果转化成对应的值或对象,例如:int、long、bool、NSString、NSDate、NSData、char *、 id等。
FMDatabaseQueue:
用来管理数据查询的队列,保证大部分时间下对数据库的操作是串行的。
FMDatabaseAdditions:
作为
FMDatabase
类的拓展。新增了一些常用的校验方法,例如:表是否存在、列是否存在、版本号、sql校验等。
FMDatabasePool:
用来管理数据库查询任务。不过在头文件中,作者写的非常清楚墙裂不建议使用,而是用 FMDatabaseQueue
代替。如果一定要用的话,一定要注意死锁。
FMDatabase
- 表示单个SQLite数据库。用于执行SQL语句。
- 不要实例化单个
FMDatabase
对象并在多个线程中使用它。用FMDatabaseQueue
代替。 - 本文中会对重点方法详细解析,其他的方法一掠而过。
方法包含
+ (NSString*)FMDBUserVersion;
FMDB版本+ (NSString*)sqliteLibVersion;
sqliteLib版本号- (BOOL)open
打开数据库,并返回状态标识
打开数据库
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open([self sqlitePath], &_db );
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
}
if (_maxBusyRetryTimeInterval > 0.0) {
// set the handler
[self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
}
return YES;
}
- 判断当前db是否存在,存在则直接返回状态0
- 不存在的话,调用底层的
sqlite3_open
方法,传入两个参数,数据库的localPath和db的内存地址,并且返回执行的状态结果。 - 如果状态是
SQLITE_OK
则继续向下执行。 maxBusyRetryTimeInterval
初始化的时候默认设置为2。
static int FMDBDatabaseBusyHandler(void *f, int count) {
FMDatabase *self = (__bridge FMDatabase*)f;
if (count == 0) {
self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate];
return 1;
}
NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime);
if (delta < [self maxBusyRetryTimeInterval]) {
sqlite3_sleep(50); // milliseconds
return 1;
}
return 0;
}
- (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout {
_maxBusyRetryTimeInterval = timeout;
if (!_db) {
return;
}
if (timeout > 0) {
sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self));
}
else {
// turn it off otherwise
sqlite3_busy_handler(_db, nil, nil);
}
}
FMDBDatabaseBusyHandler
注册一个回调来处理SQLITE_BUSY错误- 它的
sqlite3_busy_handler(D,X,P)
例程设置了一个回调函数X,当另一个线程或进程将表锁定时,只要尝试访问与[database connection]
D关联的数据库表,就可以用参数P调用它。sqlite3_busy_handler()
接口用于实现[sqlite3_busy_timeout()]
和[PRAGMA busy_timeout]。
- 如果
busy callBack
是NULL
,则遇到锁后里面返回SQLITE_BUSY
。如果busy callBack
不是 NULL,则可以使用两个参数作为回调。 busy handler
的第一个参数是 void * 指针的副本,同时他也是sqlite3_busy_handler()
的第三个参数。sqlite3_busy_handler
的第二个参数是需要回调的busy handler
的次数,代表前面相同locking event
的次数- 如果
busy callback
返回0,则不会进行其他尝试来访问数据库,直接返回SQLITE_BUSY
,如果不是0,则再次尝试访问数据库并重复循环。 busy handler
并不能确保有 在lock contention
的时候被调用。如果SQLite
判定在调用busy handler
的时候会造成死锁,则会直接返回SQLITE_BUSY
,而不再调用busy handler
- 考虑到一个场景,其中一个线程持有一个
read lock
尝试提升为reserved lock
,另一个线程持有一个reserved lock
尝试提升为exclusive lock
。这个时候,第一个线程无法进行,因为它被第二个 blocked;第二个也没有办法进行,因为它被第一个blocked。如果两个线程都调用了busy handlers
,则两者都不会成功。因此,SQLite为第一个线程返回 SQLITE_BUSY,希望第一个线程释放其 read lock,并且第二个线程可以继续。 - callback 的默认值是NULL
- 每个
[database connection]
只能设置一个busy handler
.设置新的handler
的时候,需要提前清除之前的handler
。注意:调用[sqlite3_busy_timeout()]
或者计算[PRAGMA busy_timeout=N]
将会改变busy handler
从而清除之前的设置。 busy callback
不应执行任何修改调用busy handler
的数据库连接操作。换句话说,busy handler
是不允许重入的。任何此类操作都会导致未定义的行为。busy handler
不能关闭数据库连接,也不能调用[prepared statement]
方法。
执行语句
- (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;
FMStatement *statement = 0x00;
FMResultSet *rs = 0x00;
if (_traceExecution && sql) {
NSLog(@"%@ executeQuery: %@", self, sql);
}
if (_shouldCacheStatements) {
statement = [self cachedStatementForQuery:sql];
pStmt = statement ? [statement statement] : 0x00;
[statement reset];
}
if (!pStmt) {
rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
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();
}
sqlite3_finalize(pStmt);
_isExecutingStatement = NO;
return nil;
}
}
id obj;
int idx = 0;
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)
// If dictionaryArgs is passed in, that means we are using sqlite's named parameter support
if (dictionaryArgs) {
for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
// Prefix the key with a colon.
NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
if (_traceExecution) {
NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
}
// Get the index for the parameter name.
int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
FMDBRelease(parameterName);
if (namedIdx > 0) {
// Standard binding from here.
[self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
// increment the binding count, so our check below works out
idx++;
}
else {
NSLog(@"Could not find index for %@", dictionaryKey);
}
}
}
else {
while (idx < queryCount) {
if (arrayArgs && idx < (int)[arrayArgs count]) {
obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
}
else if (args) {
obj = va_arg(args, id);
}
else {
//We ran out of arguments
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
if (!statement) {
statement = [[FMStatement alloc] init];
[statement setStatement:pStmt];
if (_shouldCacheStatements && sql) {
[self setCachedStatement:statement forQuery:sql];
}
}
// the statement gets closed in rs's dealloc or [rs close];
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[_openResultSets addObject:openResultSet];
[statement setUseCount:[statement useCount] + 1];
FMDBRelease(statement);
_isExecutingStatement = NO;
return rs;
}
- 执行sql 成功的话会返回
FMResultSet
对象,失败的话返回nil
;和执行更新语句一样,有一个变量接收error对象。你可以用lastErrorMessage
和lastErrorMessage
方法来确定查询失败的原因。 - 为了迭代查询结果,通常情况下会使用“while()”循环。通过
<[FMResultSet next]>
来实现从一个记录到另一个记录切换。 - 这个方法使用
sqlite3_bind
可选的参数值(sqlite.org/c3ref/bind_… )。可以正确地转义任何需要转义序列的字符(例如引号),从而消除简单的SQL错误并防止SQL注入攻击。本地处理nsstring
、nsnumber
、“nsnull
”、“nsdate
”和“nsdata
”对象。所有其他对象类型将使用对象的“description
”方法解释为文本值。 sql
参数,SELECT statement
可以使用 ?来占位。- 可选参数中的
?
只能是OC对象(例如nsstring
、nsnumber
等),而不是基本的c数据类型(例如“int
”、“char
”等)。 - 判断数据库是否存在,不存在返回
0x00(nil)
- 判断是不是在执行
statement
,在执行的话,提示数据库正在使用,并返回0x00
。 - 然后将
isExecutingStatement
置为 yes,开始进行下面的处理。 - 根据
shouldCacheStatements
字段来判断是不是缓存传入的Statements
,缓存了的话,通过sql作为key取出statementsSets
,如果取出的对象是prepare
的statement
则赋值给sqlite3_stmt
- 如果
pStmt
不存在,则调用sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0);
返回成功或失败状态,并将准备好的statement
值放到pStmt
中。 int sqlite3_bind_parameter_count(sqlite3_stmt*)
返回[SQL parameters]
参数的个数dictionaryArgs
遍历里面的参数名的值,通过该值拿到name对应的index[self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]
将传入的参数和前面sql的?
值绑定idx
用来遍历参数的个数,如果没有传入dictionaryArgs
参数,传入的而是arrayArgs
,则通过遍历数组的拿到对应的obj
绑定到idx
位置,即:将通配符?:age
按照索引 赋值为obj
- 如果数组中的参数个数,和
pStmt
中参数的个数不同,则抛出错误 - 将参数和
?
绑定完的pStmt
赋值给FMStatement
,如果需要缓存,则将sql
作为key
,FMStatement
作为object
对象放到缓存的字典里面 - 后面就是将
statement
赋值给FMResultSet
执行操作。作者特意提到 在 rs的dealloc
或者[rs close]
会将statement close
更新
- (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
...
绑定完数据,生成最终的 pStmt
rc = sqlite3_step(pStmt);
...
}
- 前面的步骤和execute都是一致的,只有获取rc的方式不一样,
sqlite3_step
拓展:
sqlite3_step()
这个过程用于执行有前面sqlite3_prepare创建的准备语句。这个语句执行到结果的第一行可用的位置。继续前进到结果的第二行的话,只需再次调用sqlite3_step()。继续调用sqlite3_setp()知道这个语句完成,那些不返回结果的语句(如:INSERT,UPDATE,或DELETE),sqlite3_step()只执行一次就返回
函数的返回值基于创建sqlite3_stmt参数所使用的函数,假如是使用老版本的接口sqlite3_prepare()和sqlite3_prepare16(),返回值会是 SQLITE_BUSY, SQLITE_DONE, SQLITE_ROW, SQLITE_ERROR 或 SQLITE_MISUSE,而v2版本的接口sqlite3_prepare_v2()和sqlite3_prepare16_v2()则会同时返回这些结果码和扩展结果码。
对所有V3.6.23.1以及其前面的所有版本,需要在sqlite3_step()之后调用sqlite3_reset(),在后续的sqlite3_ step之前。如果调用sqlite3_reset重置准备语句失败,将会导致sqlite3_ step返回SQLITE_MISUSE,但是在V3. 6.23.1以后,sqlite3_step()将会自动调用sqlite3_reset。
- 每调用一次
[cachedStmt useCount] + 1
转换
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... {
va_list args;
va_start(args, format);
NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]];
NSMutableArray *arguments = [NSMutableArray array];
[self extractSQL:format argumentsList:args intoString:sql arguments:arguments];
va_end(args);
return [self executeQuery:sql withArgumentsInArray:arguments];
}
该方法其实调用的是- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments
,其中 sql
为 SELECT * FROM t_student WHERE age > %d
,cleanedSQL
为转换完的值 SELECT * FROM t_student WHERE age > ?
加解密
- 加密
- (BOOL)setKey:(NSString*)key;
- (BOOL)setKeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
if (!keyData) {
return NO;
}
int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]);
return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
return NO;
#endif
}
- 解密
- (BOOL)rekey:(NSString*)key;
- (BOOL)rekeyWithData:(NSData *)keyData {
#ifdef SQLITE_HAS_CODEC
if (!keyData) {
return NO;
}
int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]);
if (rc != SQLITE_OK) {
NSLog(@"error on rekey: %d", rc);
NSLog(@"%@", [self lastErrorMessage]);
}
return (rc == SQLITE_OK);
#else
#pragma unused(keyData)
return NO;
#endif
}
FMDatabaseAdditions
该类作为 FMDatabase 的补充,添加了一些常用的方法
-(BOOL)validateSQL:(NSString)sql error:(NSError*)error;
sql的有效性
-(BOOL)tableExists:(NSString*)tableName;
数据库表是否存在。
-(BOOL)columnExists:(NSString)columnName inTableWithName:(NSString)tableName;
在tableName表中columnName是否存在。
-(FMResultSet*)getSchema;
数据库的一些概要信息
写在最后
1.欢迎大家对文章给出建议或意见。
2.本文凝结了作者的心血,希望大家在转发、传阅的时候能够保留文章的初始地址。
相关链接:
参考链接: 1.www.sqlite.org/index.html