FMDB 源码解析 FMDatabase类

1,824 阅读9分钟

前言

在上篇文章中介绍了文件的组成并详细的介绍了 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 代替。
  • 本文中会对重点方法详细解析,其他的方法一掠而过。

方法包含

  1. + (NSString*)FMDBUserVersion; FMDB版本
  2. + (NSString*)sqliteLibVersion;sqliteLib版本号
  3. - (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 callBackNULL,则遇到锁后里面返回 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对象。你可以用 lastErrorMessagelastErrorMessage 方法来确定查询失败的原因。
  • 为了迭代查询结果,通常情况下会使用“while()”循环。通过<[FMResultSet next]>来实现从一个记录到另一个记录切换。
  • 这个方法使用 sqlite3_bind可选的参数值(sqlite.org/c3ref/bind_… )。可以正确地转义任何需要转义序列的字符(例如引号),从而消除简单的SQL错误并防止SQL注入攻击。本地处理 nsstringnsnumber、“nsnull”、“nsdate”和“nsdata”对象。所有其他对象类型将使用对象的“description”方法解释为文本值。
  • sql 参数,SELECT statement 可以使用 ?来占位。
  • 可选参数中的 只能是OC对象(例如 nsstringnsnumber 等),而不是基本的c数据类型(例如“int”、“char”等)。
  • 判断数据库是否存在,不存在返回 0x00(nil)
  • 判断是不是在执行 statement,在执行的话,提示数据库正在使用,并返回 0x00
  • 然后将 isExecutingStatement置为 yes,开始进行下面的处理。
  • 根据 shouldCacheStatements 字段来判断是不是缓存传入的 Statements ,缓存了的话,通过sql作为key取出 statementsSets,如果取出的对象是 preparestatement 则赋值给 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 作为 keyFMStatement 作为 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,其中 sqlSELECT * FROM t_student WHERE age > %dcleanedSQL 为转换完的值 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.FMDB 源码解析 FMResultSet类

2.FMDB 源码解析 FMDatabase类

参考链接: 1.www.sqlite.org/index.html

  1. www.code4app.com/home.php?mo…

  2. www.jianshu.com/p/967a213a4…