iOS之FMDB 源码解读

FMDBSQLite3 的C函数做了面向对象的封装:针对数据库提供了增删查改等接口,以及事务处理 等,并通过一个GCD的串行队列保证在多线程环境下的数据安全。
这些功能主要封装在以下几个类中:

  • FMStatement :是对 SQLite 的预处理语句 sqlite3_stmt 的封装,并增加了缓存该语句的功能;
  • FMDatabase:代表一个单独的SQLite操作实例,打开或者关闭数据库,对数据库执行增、删、改、查等操作,以及以事务方式处理数据等;
  • FMResultSet:封装了查询后的结果集,通过 -next 获取查询的数据;
  • FMDatabaseQueue:将对数据库的所有操作,都封装在串行队列执行!避免数据竞态问题,保证多线程环境下的数据安全;
  • FMDatabaseAdditions:是FMDatabase的分类,扩展了查找表是否存在,版本号,表信息等功能;
  • FMDatabasePoolFMDatabase的对象池封装,在多线程环境中访问单个FMDatabase对象容易引起问题,可以通过FMDatabasePool对象池来解决多线程下的访问安全问题;不推荐使用,优先使用FMDatabaseQueue

1、FMStatement

Cached Statement功能

我们执行的 Sql 语句,需要通过 sqlite3_prepare_v2() 函数编译为 预处理语句 sqlite3_stmt,然后再由 sqlite3_step() 函数执行该语句!这些过程明显都是耗时操作,如果执行的 SQL 语句大量重复,为提升操作效率,FMDB 提供了缓存这些 sqlite3_stmt 语句的功能!
FMDatabaseshouldCacheStatements 属性用来管理是否缓存这些语句, cachedStatements 字典用来缓存这些语句!而 FMStatement 通过对 sqlite3_stmt 的封装,作为一个元素被存储在字典 cachedStatements 中!

1.1、sqlite3_stmt 的状态记录
/** 从缓存中取出 sqlite3_stmt ,并重复使用的次数 */
@property (atomic, assign) long useCount;

/** 需要执行的 SQL 语句 */
@property (atomic, retain) NSString *query;

/** 预处理语句 sqlite3_stmt :将 Sql 通过 sqlite3_prepare_v2() 函数编译而来 */
@property (atomic, assign) void *statement;

/** 是否正在使用该条语句 */
@property (atomic, assign) BOOL inUse;
1.2、释放sqlite3_stmt

sqlite3_stmt 作为 C 变量,需要调用它的析构函数 sqlite3_finalize()释放内存,否则会造成内存泄漏!

/** 释放 sqlite3_stmt 内存 */
- (void)close {
    if (_statement) {
        sqlite3_finalize(_statement);
        _statement = 0x00;
    }
    _inUse = NO;
}
1.3、重置sqlite3_stmt

sqlite3_stmt 是可以被 sqlite3_step() 函数多次调用的,每调用一次,它就会向下执行一次:

  • 对于使用 SELECT 命令筛选数据时,可以使用 while() 循环多次调用,获取结果集中的一条条数据;
  • 对于 CREATE 、DROP 、INSERT 、DELETE、UPDATE 等 Sqlite 命令,只需执行一次,再次执行是没有意义的,sqlite3_step() 函数会返回 SQLITE_ROW 表明该 sqlite3_stmt 已经被执行过一次!

因此,针对缓存的 sqlite3_stmt,从缓存中取出之后,需要先重置该语句;以免被缓存前的其它操作所影响。

/** 重置  */
- (void)reset {
    if (_statement) {
        sqlite3_reset(_statement);
    }
    _inUse = NO;
}

2、FMDatabase

FMDatabase代表一个单独的SQLite操作实例,打开或者关闭数据库,对数据库执行增、删、改、查等操作,以及以事务方式处理数据等!

2.1、实例化一个 FMDatabase
+ (instancetype)databaseWithPath:(NSString * _Nullable)inPath;
+ (instancetype)databaseWithURL:(NSURL * _Nullable)url;
- (instancetype)initWithURL:(NSURL * _Nullable)url;
- (instancetype)initWithPath:(NSString * _Nullable)path{
    assert(sqlite3_threadsafe()); 
    self = [super init];
    if (self) {
        _databasePath               = [path copy];
        _openResultSets             = [[NSMutableSet alloc] init];
        _db                         = nil;//数据库为空
        _logsErrors                 = YES;
        _crashOnErrors              = NO;
        _maxBusyRetryTimeInterval   = 2;//默认为2
        _isOpen                     = NO;//默认数据库关闭
    }
    return self;
}

创建 FMDatabase 实例时数据库并没有被创建,只是给了数据库一个名字!-open 方法调用 sqlite3_open() 函数才真正获取到数据库!

针对该数据库的路径,可能出现以下情况:

  • 1、文件路径:如果在磁盘里该文件不存在,则该方法内部自动创建;
  • 2、路径为 nil :创建数据库缓存在 memory 中,关闭 FMDatabase 连接后,此数据库将被销毁。
  • 3、空字符串@“” :在tmp目录下创建一个空数据库。关闭 FMDatabase 连接后删除此数据库;
2.2、打开和关闭数据库
/// 数据库是否打开
@property (nonatomic) BOOL isOpen;

/** 打开数据库  */
- (BOOL)openWithFlags:(int)flags vfs:(NSString * _Nullable)vfsName;
- (BOOL)openWithFlags:(int)flags;
- (BOOL)open{
    if (_isOpen) {
        return YES;
    }
    if (_db) {// 如果之前尝试打开但失败,请确保在再次尝试之前关闭它
        [self close];
    }
    
    //调用sqlite3_open()函数创建数据库
    int err = sqlite3_open([self sqlitePath], (sqlite3**)&_db);
    if(err != SQLITE_OK) {
        NSLog(@"error opening!: %d", err);
        return NO;
    }
    
    //当执行这段代码的时候,数据库正在被其他线程访问,就需要给他设置重试时间,
    if (_maxBusyRetryTimeInterval > 0.0) {
        [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval];
    }
    _isOpen = YES;
    return YES;
}


/** 关闭数据库连接 */
- (BOOL)close {
    [self clearCachedStatements];//清理缓存
    [self closeOpenResultSets];//清理结果集
    if (!_db) {//如果数据库不存在
        return YES;
    }
    
    int  rc;
    BOOL retry;
    BOOL triedFinalizingOpenStatements = NO;
    do {
        retry   = NO;
        rc      = sqlite3_close(_db);
        if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
            if (!triedFinalizingOpenStatements) {
                triedFinalizingOpenStatements = YES;
                sqlite3_stmt *pStmt;
                while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
                    NSLog(@"Closing leaked statement");
                    sqlite3_finalize(pStmt);
                    retry = YES;
                }
            }
        }else if (SQLITE_OK != rc) {
            NSLog(@"error closing!: %d", rc);
        }
    }
    while (retry);
    _db = nil;//数据库指针指向 nil
    _isOpen = false;//状态改为 NO
    return YES;
}

/** 中断数据库操作 */
- (BOOL)interrupt{
    if (_db) {
        sqlite3_interrupt([self sqliteHandle]);
        return YES;
    }
    return NO;
}

/** 确认数据库是否有良好的连接:
 *   1、数据库是打开的;
 *   2、如果打开,将尝试一个简单的 SELECT 语句并确认成功
 * @return 一切顺利则返回 YES;否则返回 NO
 */
@property (nonatomic, readonly) BOOL goodConnection;

数据库的打开与关闭,都是耗时操作!因此需要创建一个单例类打开数据库,所有的操作就封装该类的方法中!

2.3、缓存与结果集
2.3.1、缓存
/** 是否应该缓存 statements
 * 针对大量重复的 Sql 语句,通过缓存,可以提升程序的性能;但也要注意它对内存的消耗也是非常大的。
 */
@property (nonatomic) BOOL shouldCacheStatements;

/** 缓存 FMStatement 的字典  */
@property (atomic, retain, nullable) NSMutableDictionary *cachedStatements;

/** 清除缓存语句 */
- (void)clearCachedStatements;
配置缓存
/** 重写 shouldCacheStatements 的 -set 方法
 * 根据情况为字典 cachedStatements 赋值
 */
- (void)setShouldCacheStatements:(BOOL)value {
    _shouldCacheStatements = value;
    if (_shouldCacheStatements && !_cachedStatements) {
        [self setCachedStatements:[NSMutableDictionary dictionary]];
    }
    if (!_shouldCacheStatements) {
        [self setCachedStatements:nil];//清空缓存数据
    }
}
查询与设置缓存
/** 查询缓存语句 */
- (FMStatement*)cachedStatementForQuery:(NSString*)query {
    NSMutableSet* statements = [_cachedStatements objectForKey:query];
    return [[statements objectsPassingTest:^BOOL(FMStatement* statement, BOOL *stop) {
        *stop = ![statement inUse];
        return *stop;
    }] anyObject];
}

/** 设置缓存语句 */
- (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query {
    NSParameterAssert(query);
    if (!query) {
        NSLog(@"API misuse, -[FMDatabase setCachedStatement:forQuery:] query must not be nil");
        return;
    }
    query = [query copy];
    [statement setQuery:query];
    NSMutableSet* statements = [_cachedStatements objectForKey:query];
    if (!statements) {
        statements = [NSMutableSet set];
    }
    [statements addObject:statement];
    [_cachedStatements setObject:statements forKey:query];
    FMDBRelease(query);
}
清除缓存
/** 清除缓存语句 */
- (void)clearCachedStatements {
    /** 1、首先遍历字典,将 FMStatement 持有的 sqlite3_stmt 全部释放 */
    for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) {
        for (FMStatement *statement in [statements allObjects]) {
            [statement close];//释放 sqlite3_stmt
        }
    }
    /** 2、其次将字典中的元素全部移除 */
    [_cachedStatements removeAllObjects];
}
2.3.2、结果集

对于 -executeQuery 方法查询数据得到的结果集 FMResultSetFMDatabase 总会将其存储在数组 _openResultSets 上!当然,FMDatabase 也提供了删除这些数据的方法:

/** 关闭结所有打开的结果集 ***/
- (void)closeOpenResultSets {
    NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]);
    for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
        FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
        [rs setParentDB:nil];
        [rs close];
        [_openResultSets removeObject:rsInWrappedInATastyValueMeal];
    }
}

/** 是否有打开的结果集 ***/
- (BOOL)hasOpenResultSets {
    return [_openResultSets count] > 0;
}

但是,假如 FMResultSet 实例调用 -close 关闭该结果集,那么又如何同步删除数组 _openResultSets 上的元素呢?

@implementation FMResultSet

- (void)close {
    [_statement reset];
    FMDBRelease(_statement);
    _statement = nil;
    /** FMResultSet 关闭时,需要FMDatabase从 _openResultSets 移除该结果集 */
    [_parentDB resultSetDidClose:self];
    [self setParentDB:nil];
}
@end

可以看到 FMResultSet-close 方法中调用了FMDatabase-resultSetDidClose 方法:

/** FMResultSet 关闭时,需要FMDatabase从 _openResultSets 移除该结果集 */
- (void)resultSetDidClose:(FMResultSet *)resultSet {
    NSValue *setValue = [NSValue valueWithNonretainedObject:resultSet];
    [_openResultSets removeObject:setValue];
}

如此,就做到了数据同步!

2.4、执行更新
2.4.1、-executeUpdate 系列

FMDB 的 -executeUpdate 系列方法可以执行 CREATEDROPINSERTDELETEUPDATE 等 Sqlite 语句!

/** 执行单个更新语句 
 * 如果语句执行失败,可以通过双指针 `NSError`获取错误原因;也可以调用 `-lastError`、 `-lastErrorCod` 或 `-lastErrorMessage` 获取失败信息。
 */
- (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ...;
- (BOOL)executeUpdate:(NSString*)sql, ...;
- (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError * _Nullable __autoreleasing *)outErr, ...  __deprecated_msg("Use executeUpdate:withErrorAndBindings: instead");;
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments;
- (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args;
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);

上述方法,其实质都是执行 -executeUpdate:error:withArgumentsInArray:orDictionary:orVAList:

//数据库是否存在:打开即存在
- (BOOL)databaseExists {
    return _isOpen;
}

- (BOOL)executeUpdate:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
    /********** 判断环境 ********/
    if (![self databaseExists]) {//数据库是否存在
        return NO;
    }
    if (_isExecutingStatement) {//正在执行 Sql
        [self warnInUse];
        return NO;
    }
    _isExecutingStatement = YES;
    
    int rc                   = 0x00;
    sqlite3_stmt *pStmt      = 0x00;//预处理语句
    FMStatement *cachedStmt  = 0x00;//缓存语句
    
    if (_traceExecution && sql) {
        NSLog(@"%@ executeUpdate: %@", self, sql);
    }
    
    /********** 获取缓存数据 ********/
    if (_shouldCacheStatements) {
        cachedStmt = [self cachedStatementForQuery:sql];//取出缓存的 FMStatement
        pStmt = cachedStmt ? [cachedStmt statement] : 0x00;//获取预处理语句 sqlite3_stmt
        [cachedStmt reset];//重置 sqlite3_stmt
    }
    
    /********** 没有缓存则调用 sqlite3_prepare_v2() 创建 sqlite3_stmt ********/
    if (!pStmt) {
        //对sql语句进行编译,创建 sqlite3_stmt
        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();
            }
            if (outErr) {
                *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]];
            }
            sqlite3_finalize(pStmt);//失败则释放 sqlite3_stmt
            _isExecutingStatement = NO;
            return NO;
        }
    }
    
    id obj;
    int idx = 0;
    int queryCount = sqlite3_bind_parameter_count(pStmt);
    
    /********** 将变量绑定到 sqlite3_stmt 上 ********/
    if (dictionaryArgs) {
        for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
            NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
            if (_traceExecution) {
                NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
            }
             // 获取参数名的索引: 第几列
            int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
            FMDBRelease(parameterName);
            if (namedIdx > 0) {
                // 将指定的 value 绑定到 sqlite3_stmt 上 指定的列数
                [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
                idx++;// 计量绑定的参数
            }else {
                NSString *message = [NSString stringWithFormat:@"Could not find index for %@", dictionaryKey];
                if (_logsErrors) {
                    NSLog(@"%@", message);
                }
                if (outErr) {
                    *outErr = [self errorWithMessage:message];
                }
            }
        }
    }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++;// 计量绑定的参数
            // 将指定的 value 绑定到 sqlite3_stmt 上 指定的列数
            [self bindObject:obj toColumn:idx inStatement:pStmt];
        }
    }
    if (idx != queryCount) {//如果绑定的参数数目不对,则进行出错处理
        NSString *message = [NSString stringWithFormat:@"Error: the bind count (%d) is not correct for the # of variables in the query (%d) (%@) (executeUpdate)", idx, queryCount, sql];
        if (_logsErrors) {
            NSLog(@"%@", message);
        }
        if (outErr) {
            *outErr = [self errorWithMessage:message];
        }
        
        sqlite3_finalize(pStmt);
        _isExecutingStatement = NO;
        return NO;
    }
    
    /** 用于执行有前面 sqlite3_prepare() 创建的 sqlite3_stmt 语句。
     * 该函数执行到结果的第一行可用的位置,继续前进到结果的第二行的话,只需再次调用sqlite3_setp()。
     * 由于执行的SQL不是 SELECT 语句,假设不会返回任何数据,此处 sqlite3_setp() 只调用一次。
     */
    rc = sqlite3_step(pStmt);//执行预处理语句
    if (SQLITE_DONE == rc) {
        //sqlite3_step() 完成执行操作
    }else if (SQLITE_INTERRUPT == rc) {
        //操作被 sqlite3_interupt() 函数中断
        if (_logsErrors) {
            NSLog(@"Error calling sqlite3_step. Query was interrupted (%d: %s) SQLITE_INTERRUPT", rc, sqlite3_errmsg(_db));
            NSLog(@"DB Query: %@", sql);
        }
    }else if (rc == SQLITE_ROW) {
        // sqlite3_step() 已经产生一个行结果 : 即 sqlite3_stmt 被执行过了一次
        NSString *message = [NSString stringWithFormat:@"A executeUpdate is being called with a query string '%@'", sql];
        if (_logsErrors) {
            NSLog(@"%@", message);
            NSLog(@"DB Query: %@", sql);
        }
        if (outErr) {
            *outErr = [self errorWithMessage:message];
        }
    }else {
        if (outErr) {
            *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]];
        }
        
        if (SQLITE_ERROR == rc) {// SQL错误 或 丢失数据库
            if (_logsErrors) {
                NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(_db));
                NSLog(@"DB Query: %@", sql);
            }
        }else if (SQLITE_MISUSE == rc) {//不正确的库使用
            if (_logsErrors) {
                NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(_db));
                NSLog(@"DB Query: %@", sql);
            }
        }else {
            if (_logsErrors) {
                NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(_db));
                NSLog(@"DB Query: %@", sql);
            }
        }
    }
    
   
    /**********  针对缓存的处理  ********/
    if (_shouldCacheStatements && !cachedStmt) {//没有缓存,且需要缓存,则创建 FMStatement 对象并缓存
        cachedStmt = [[FMStatement alloc] init];
        [cachedStmt setStatement:pStmt];
        [self setCachedStatement:cachedStmt forQuery:sql];
        FMDBRelease(cachedStmt);
    }
    int closeErrorCode;
    
    if (cachedStmt) {//对缓存数据的处理
        [cachedStmt setUseCount:[cachedStmt useCount] + 1];//计算 sqlite3_stmt 使用过的次数
        closeErrorCode = sqlite3_reset(pStmt);//重置一个pStmt语句对象到它的初始状态,然后准备被重新执行。
    }else {
        //如果不需要缓存,则释放 sqlite3_stmt
        closeErrorCode = sqlite3_finalize(pStmt);
    }
    
    if (closeErrorCode != SQLITE_OK) {
        if (_logsErrors) {
            NSLog(@"Unknown error finalizing or resetting statement (%d: %s)", closeErrorCode, sqlite3_errmsg(_db));
            NSLog(@"DB Query: %@", sql);
        }
    }
    
    _isExecutingStatement = NO;
    return (rc == SQLITE_DONE || rc == SQLITE_OK);
}

该方法主要做了几件事:

  • 1、判断环境:数据库是否存在、或者是否正在执行 Sql 语句;
  • 2、如果有缓存,则获取缓存,并重置预处理语句 sqlite3_stmt
  • 3、如果没有缓存,则调用 sqlite3_prepare_v2() 函数创建 sqlite3_stmt
  • 4、将 Sql 语句中占位符 '?' 所对应的变量值通过 sqlite3_bind_*() 函数绑定到结构 sqlite3_stmt 上;
  • 5、调用 sqlite3_step() 函数执行预处理语句 sqlite3_stmt
  • 6、针对缓存的处理:如果需要则缓存,否则释放预处理语句 sqlite3_stmt

第 4 件事 将 Sql 语句中占位符 '?' 所对应的变量值绑定到sqlite3_stmt 主要通过-bindObject:toColumn:inStatement:方法完成:

- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
    if ((!obj) || ((NSNull *)obj == [NSNull null])) {//obj 为 nil
        sqlite3_bind_null(pStmt, idx);
    }else if ([obj isKindOfClass:[NSData class]]) {//绑定 NSData
        const void *bytes = [obj bytes];
        if (!bytes) {
            // 一个空的 NSData 对象, 即 [NSData data].
            // 不要传递空指针,否则sqlite将绑定一个 SQL NULL 而不是一个blob。
            bytes = "";
        }
        sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
    }else if ([obj isKindOfClass:[NSDate class]]) {// NSDate
        if (self.hasDateFormatter)
            sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC);
        else
            sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]);
    }else if ([obj isKindOfClass:[NSNumber class]]) {// NSNumber
        if (strcmp([obj objCType], @encode(char)) == 0) {//char 型
            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) {//short 型
            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) {//int 型
            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) {//long 型
            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) {//long long 型
            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) {//float 型
            sqlite3_bind_double(pStmt, idx, [obj floatValue]);
        }else if (strcmp([obj objCType], @encode(double)) == 0) {//double 型
            sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
        }else if (strcmp([obj objCType], @encode(BOOL)) == 0) {//BOOL 型
            sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
        }else {//text 型
            sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
        }
    }else {// text
        sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
    }
}

上述方法的主要功能就是调用 sqlite3_bind_*() 函数将值 obj 绑定预处理语句 sqlite3_stmt

  • @param obj 待绑定的值;
  • @param idx 表中所在列数的索引,需要将 obj 绑定到第几列;
  • @parma pStmt 预处理语句,需要将 obj 绑定到该结构上;

SQLite的 Sql 语句通过绑定变量,减少SQL语句被动态解析的次数,从而提高数据查询和数据操作的效率。

通过-bindObject:toColumn:inStatement:方法内部实现,我们可以发现:SQL语句只能接收的Objective-C对象如 NSStringNSNumberNSNullNSDateNSData 等;基本数据类型 intNSIntegerfloat需要转化为 NSNumber;其它Objective-C对象,将使用对象的 -description 方法的字符串。

2.4.2 、不得不说的 -executeUpdateWithFormat:方法
- (BOOL)executeUpdateWithFormat:(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 executeUpdate:sql withArgumentsInArray:arguments];
}

上述方法执行 Sql 语句之前,先调用了 -extractSQL:argumentsList:intoString:arguments:

/** 将对应字符串处理成相应的 SQL 语句:
 * 针对 WithFormat:@"INSERT INTO test (name) VALUES (%@)", @"Gus"
 * 将其中的 %s 、%d 、 %@ 等转义序列变为占位符 ? ,然后将 "Gus" 加入到 arguments 中
 */
- (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments;

对于需要包含某些特殊字符:使用 '?' 占位符比 [NSString stringWithFormat:] 手动构建 SQL 语句更安全:

[db executeUpdateWithFormat:@"INSERT INTO Persions (name) VALUES (%@)", @"Gus"];
[db executeUpdate:@"INSERT INTO Persions (name) VALUES (?)", @"Gus”];
2.4.3 、其它方法
/** 使用 Block 执行多个SQL语句更新 */
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(__attribute__((noescape)) FMDBExecuteStatementsCallbackBlock _Nullable)block;
- (BOOL)executeStatements:(NSString *)sql;

#pragma mark 更新语句信息

/** 获取最后插入一行的主键 rowid
 * 通过 sqlite3_last_insert_rowid()  函数实现
 * @note 如果数据库连接上从未发生过成功的 INSERT,则返回 0
 */
- (sqlite_int64)lastInsertRowId {
    if (_isExecutingStatement) {//正在执行 Sql 语句
        [self warnInUse];
        return NO;
    }
    _isExecutingStatement = YES;
    sqlite_int64 ret = sqlite3_last_insert_rowid(_db);
    _isExecutingStatement = NO;
    return ret;
}

/** 前一个 SQL 语句更改了多少行
 * 通过 sqlite3_changes()  函数实现
 * @note 只计算由INSERT、UPDATE或DELETE语句直接指定的更改的数据库行数
*/
- (int)changes {
    if (_isExecutingStatement) {//正在执行 Sql 语句
        [self warnInUse];
        return 0;
    }
    _isExecutingStatement = YES;
    int ret = sqlite3_changes(_db);
    _isExecutingStatement = NO;
    return ret;
}
2.5、查询

FMDB 的 -executeQuery 系列方法封装了Sqlite 的 SELECT 命令:

- (FMResultSet * _Nullable)executeQuery:(NSString*)sql, ...;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary * _Nullable)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withVAList:(va_list)args;
- (FMResultSet * _Nullable)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);

这些方法本质都去执行 -executeQuery:withArgumentsInArray:orDictionary:orVAList:

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args {
    /********** 判断环境 ********/
    if (![self databaseExists]) {//判断数据库是否存在
        return 0x00;
    }
    if (_isExecutingStatement) {//是否正在执行 Sql 语句
        [self warnInUse];
        return 0x00;
    }
    _isExecutingStatement = YES;
    
    int rc                  = 0x00;
    sqlite3_stmt *pStmt     = 0x00;//预处理语句
    FMStatement *statement  = 0x00;
    FMResultSet *rs         = 0x00;//结果集
    
    if (_traceExecution && sql) {//打印sql语句
        NSLog(@"%@ executeQuery: %@", self, sql);
    }
    /********** 获取缓存数据 ********/
    if (_shouldCacheStatements) {
        statement = [self cachedStatementForQuery:sql];
        pStmt = statement ? [statement statement] : 0x00;//缓存的预处理语句
        [statement reset];
    }
        
    /********** 没有缓存则调用 sqlite3_prepare() 创建 sqlite3_stmt ********/
    if (!pStmt) {
        //对sql语句进行预处理,创建 sqlite3_stmt
        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;
        }
    }
    
    /********** 将变量绑定到 sqlite3_stmt 上 ********/
    id obj;
    int idx = 0;
    int queryCount = sqlite3_bind_parameter_count(pStmt);
    if (dictionaryArgs) {
        for (NSString *dictionaryKey in [dictionaryArgs allKeys]) {
            NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey];
            if (_traceExecution) {
                NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]);
            }
            // 获取参数名的索引: 第几列
            int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]);
            FMDBRelease(parameterName);
            if (namedIdx > 0) {
                // 将指定的 value 绑定到 sqlite3_stmt 上 指定的列数
                [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt];
                idx++;//计量绑定的参数
            }else {
                NSLog(@"Could not find index for %@", dictionaryKey);
            }
        }
    } else {//对于arrayArgs参数和不定参数的处理,类似于"?"参数形式
        while (idx < queryCount) {
            if (arrayArgs && idx < (int)[arrayArgs count]) {
                obj = [arrayArgs objectAtIndex:(NSUInteger)idx];
            }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++;//计量绑定的参数
            // 将指定的 value 绑定到 sqlite3_stmt 上 指定的列数
            [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);
    
    /********** 没有 FMStatement 则创建 FMStatement 对象  ********/
    if (!statement) {
        statement = [[FMStatement alloc] init];
        [statement setStatement:pStmt];
        if (_shouldCacheStatements && sql) {
            //缓存的处理,key为sql语句,值为statement
            [self setCachedStatement:statement forQuery:sql];
        }
    }
    
    /*************** 根据 FMStatement 实例创建一个 FMResultSet *************/
    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;
}

该方法主要做了几件事:

  • 1、判断环境:数据库是否存在、或者是否正在执行 Sql 语句;
  • 2、如果有缓存,则获取缓存,并重置预处理语句 sqlite3_stmt
  • 3、如果没有缓存,则调用 sqlite3_prepare_v2() 函数创建 sqlite3_stmt
  • 4、将 Sql 语句中占位符 所对应的变量值通过 sqlite3_bind_*() 函数绑定到结构 sqlite3_stmt 上;
  • 5、如果 FMStatement 实例为空,则创建 FMStatement 实例,并根据需要缓存该实例;
  • 6、根据 FMStatement 实例创建一个 FMResultSet ,并将FMResultSet 实例存储在数组 _openResultSets 中;

该方法本质上仅仅封装了预处理语句 sqlite3_stmt,但是并没有执行该语句;要获取查询的数据,需要调用 sqlite3_setp() 函数执行 sqlite3_stmt!FMDB 将执行语句封装在了 FMResultSet-next 方法中:

@implementation FMResultSet

- (BOOL)next {
    return [self nextWithError:nil];
}

- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr {
    int rc = sqlite3_step([_statement statement]);//执行查询
    if (rc != SQLITE_ROW) {
        [self close];
    }
    return (rc == SQLITE_ROW);
}
@end

由于这些 C 语言变量的内存不被 ARC系统所管理,因此使用 FMResultSet 实例完毕后,需要关闭结果集 [FMResultSet close]

2.6、事务
/** 确定当前是否处于事务中 */
@property (nonatomic, readonly) BOOL isInTransaction;

/** 开启事务
 * @note FMDatabase对事务的操作,操作成功返回 YES。失败调用 -lastError、-lastErrorCod 或 -lastErrorMessage 获取失败信息;
 *
 * @warning 与 SQLite 的 BEGIN TRANSACTION 不同,此方法当前执行的是互斥事务,而不是延迟事务;
 *       在FMDB的未来版本中,这种行为可能会改变,因此该方法最终可能采用标准SQLite行为并执行延迟事务。
 *       建议使用 -beginExclusiveTransaction ,保证代码在未来不会被修改。
 */
- (BOOL)beginTransaction {//默认开始互斥事务
    BOOL b = [self executeUpdate:@"begin exclusive transaction"];
    if (b) {
        _isInTransaction = YES;//标记处于事务中
    }
    return b;
}

- (BOOL)beginDeferredTransaction {//开始一个延迟的事务
    BOOL b = [self executeUpdate:@"begin deferred transaction"];
    if (b) {
        _isInTransaction = YES;//标记处于事务中
    }
    return b;
}

- (BOOL)beginImmediateTransaction {//开启即时事务
    BOOL b = [self executeUpdate:@"begin immediate transaction"];
    if (b) {
        _isInTransaction = YES;//标记处于事务中
    }
    return b;
}

- (BOOL)beginExclusiveTransaction {//开始互斥事务
    BOOL b = [self executeUpdate:@"begin exclusive transaction"];
    if (b) {
        _isInTransaction = YES;//标记处于事务中
    }
    return b;
}

/** 提交一个事务
 * 提交一个由 -beginTransaction 或 -beginDeferredTransaction 启动的事务。
 */
- (BOOL)commit {
    BOOL b =  [self executeUpdate:@"commit transaction"];
    if (b) {
        _isInTransaction = NO;//标记事务结束
    }
    return b;
}

/** 回滚事务:把数据库恢复到上次 -commit 或 -rollback 命令时的情况
 * 回滚用 -beginTransaction 或 -beginDeferredTransaction 启动的事务。
 */
- (BOOL)rollback {
    BOOL b = [self executeUpdate:@"rollback transaction"];
    if (b) {
        _isInTransaction = NO;//标记事务结束
    }
    return b;
}
2..6.1、事务中的 SavePoint
//处理 SavePoint 名字中的特殊字符
static NSString *FMDBEscapeSavePointName(NSString *savepointName) {
    return [savepointName stringByReplacingOccurrencesOfString:@"'" withString:@"''"];
}

/** 创建保存点
 * @param name 保存点的名称
 * @param outErr 双指针 NSError,记录发生的错误;如果为 nil ,则不会返回 NSError;
 * @return 成功返回 YES。如果失败,可以调用 -lastError、 -lastErrorCod 或 -lastErrorMessage 获取失败信息;
 */
- (BOOL)startSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr{
    NSParameterAssert(name);
    NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)];
    return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
}

/** 撤销保存点
 */
- (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr {
    NSParameterAssert(name);
    NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)];
    return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
}

/** 回滚到保存点
 */
- (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError * _Nullable __autoreleasing *)outErr{
    NSParameterAssert(name);
    NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)];
    return [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:nil];
}

/** 执行保存点后的代码
 * @param block 要在保存点内执行的代码块
 * @return 错误对应的NSError;如果没有错误返回nil
 */
- (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(BOOL *rollback))block{
    static unsigned long savePointIdx = 0;    
    NSString *name = [NSString stringWithFormat:@"dbSavePoint%ld", savePointIdx++];
    BOOL shouldRollback = NO;
    NSError *err = 0x00;
    if (![self startSavePointWithName:name error:&err]) {
        return err;
    }
    if (block) {
        block(&shouldRollback);
    }
    if (shouldRollback) {
        // We need to rollback and release this savepoint to remove it
        [self rollbackToSavePointWithName:name error:&err];
    }
    [self releaseSavePointWithName:name error:&err];
    return err;
}
2..6.2、事务中的检查点 Checkpoint
/** 手动配置 WAL 检查点
 * @param name sqlite3_wal_checkpoint_v2() 的数据库名称
 * @param checkpointMode  sqlite3_wal_checkpoint_v2() 的检查点类型
 * @param logFrameCount 如果不为NULL,则将其设置为日志文件中的总帧数,如果检查点由于错误或数据库没有处于WAL模式而无法运行,则将其设置为-1。
 * @param checkpointCount 如果不是NULL,那么这将检查点帧总数日志文件(包括检查点之前任何已经调用的函数),如果检查点由于错误或数据库不在WAL模式下而无法运行,则为-1。
 * @return 成功返回 YES;
 */
- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode error:(NSError * _Nullable *)error;
- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name error:(NSError * _Nullable *)error;
- (BOOL)checkpoint:(FMDBCheckpointMode)checkpointMode name:(NSString * _Nullable)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * _Nullable *)error{
    const char* dbName = [name UTF8String];
    int err = sqlite3_wal_checkpoint_v2(_db, dbName, checkpointMode, logFrameCount, checkpointCount);
    if(err != SQLITE_OK) {
        if (error) {
            *error = [self lastError];
        }
        if (self.logsErrors) NSLog(@"%@", [self lastErrorMessage]);
        if (self.crashOnErrors) {
            NSAssert(false, @"%@", [self lastErrorMessage]);
            abort();
        }
        return NO;
    } else {
        return YES;
    }
}
2.7、加密

需要购买 Sqlite 加密扩展 才能使用Sqlite 加密功能;

/** 设置加密密钥
 */
- (BOOL)setKey:(NSString*)key;

/** 重置加密密钥
 */
- (BOOL)rekey:(NSString*)key;

/** 使用 keyData 设置加密密钥
 */
- (BOOL)setKeyWithData:(NSData *)keyData;

/** 使用 keyData 重置加密密钥。
 */
- (BOOL)rekeyWithData:(NSData *)keyData;

3、FMDatabaseAdditions

FMDatabase 的分类: 用于对查询结果只返回单个值的方法进行简化;对表、列是否存在;版本号,校验SQL等等功能。

3.1、查询某个值

针对某些确定只有一个返回值的查询语句,不需要通过返回FMResultSet并遍历set获取

/** 使用SQL查询某个值:比如某个人的年纪 "SELECT age FROM Person WHERE Name = ?",@"zhangsan"
 * @param query 要执行的SQL查询
 * @note 被搜索的键最好具有唯一性,得到的结果才能唯一;
 * @note 在 Swift 中不可用
 */
- (int)intForQuery:(NSString*)query, ...;
- (long)longForQuery:(NSString*)query, ...;
- (BOOL)boolForQuery:(NSString*)query, ...;
- (double)doubleForQuery:(NSString*)query, ...;
- (NSString * _Nullable)stringForQuery:(NSString*)query, ...;
- (NSData * _Nullable)dataForQuery:(NSString*)query, ...;
- (NSDate * _Nullable)dateForQuery:(NSString*)query, ...;
3.2、数据库、表的概要信息
/** 数据库的一些概要信息  */
- (FMResultSet * _Nullable)getSchema{
    FMResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"];
    return rs;
}

/** 数据库中某张表的一些概要信息 */
- (FMResultSet * _Nullable)getTableSchema:(NSString*)tableName {
    FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"pragma table_info('%@')", tableName]];
    return rs;
}

@property (nonatomic) uint32_t applicationID;
@property (nonatomic) uint32_t userVersion;//检索用户版本
table_info

table_info 记录数据库中某张表的每一列信息!

table_info(
    cid  integer,//列码
    name text,//该列表头
    type text,//该列的数据类性
    pk    boolean,//是否是主键
    notnull boolean,//要求非空
    dflt_value,//该列默认值
)

eg: 获取表 Personstable_info

FMResultSet *resultSet = [database getTableSchema:@"Persons"];
while ([resultSet next]){
    NSLog(@"Persons Schema : %@",resultSet.resultDictionary);
}
[resultSet close];

/** 打印日志:
{ cid = 0; "dflt_value" = "<null>";   name = id;       notnull = 0;  pk = 1;  type = INTEGER; }
{ cid = 1; "dflt_value" = "<null>";   name = name;     notnull = 1;  pk = 0;  type = TEXT; }
{ cid = 2; "dflt_value" = "<null>";   name = age;      notnull = 0;  pk = 0;  type = INTEGER; }
{ cid = 3; "dflt_value" = YES;        name = sex;      notnull = 0;  pk = 0;  type = boolean;}
{ cid = 4; "dflt_value" = "datetime('now','localtime')"; name = time; notnull = 0; pk = 0; type = DATETIME;}
{ cid = 5; "dflt_value" = "'无'"; name = hobby;    notnull = 0;  pk = 0;  type = TEXT; }
*/
Sql_master

Sql_master 表是 SQLite3 的系统表。该表记录数据库中保存的表、索引、视图、和触发器信息,每一行记录一个项目。在创建一个SQLIte数据库的时候,该表会自动创建。

CREATE TABLE sqlite_master(
    type text, //系统表的创建类型,可能值 table, index, view, trigger
    name text, // 表、索引、触发器、视图的名字
    tbl_name text, //对表和视图来说,这个值和name字段一致;对索引和触发器来说是建立在那个表上的表名字
    rootpage integer, // 对表和索引来说是根页的页号;对于视图和触发器,该列值为0或者NULL;
    sql text // 创建该项目的SQL语句
 );

eg: 获取数据库中各个表的 sqlite_master

FMResultSet *resultSet = database.getSchema;
while ([resultSet next]){
    NSLog(@"Database Schema : %@",resultSet.resultDictionary);
}
[resultSet close];

/** 打印日志:
{
    name = Cars; rootpage = 8;"tbl_name" = Cars;type = table;
    sql = "CREATE TABLE Cars (id INTEGER PRIMARY KEY,owners TEXT NOT NULL,brand TEXT,price DOUBLE DEFAULT 0.0,time DATETIME DEFAULT (datetime('now','localtime')),FOREIGN KEY (owners) REFERENCES Persons(name))";
}
{
    name = Persons;rootpage = 6;"tbl_name" = Persons;type = table;
    sql = "CREATE TABLE Persons (id INTEGER PRIMARY KEY,name TEXT UNIQUE NOT NULL,age INTEGER CHECK (age>0),sex boolean DEFAULT YES,time DATETIME DEFAULT (datetime('now','localtime')),hobby TEXT DEFAULT '\U65e0', birthday TEXT)";
}
*/
3.3、指定的表、指定的列是否存在?

由于sqlite_master表存储所有的数据库项目,所以可以通过该表判断特定的表、视图或者索引是否存在!

/** 判断表是否存在 */
- (BOOL)tableExists:(NSString*)tableName {
    tableName = [tableName lowercaseString];
    FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
    BOOL returnBool = [rs next];
    [rs close];
    return returnBool;
}

由于table_info表存储着某表所有的列信息,所以可以通过该表判断特定列是否存在!

/** 测试以查看数据库中指定表是否存在指定列  */
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName {
    BOOL returnBool = NO;
    tableName  = [tableName lowercaseString];
    columnName = [columnName lowercaseString];
    FMResultSet *rs = [self getTableSchema:tableName];//获取 table_info
    while ([rs next]) {
        if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) {
            returnBool = YES;
            break;
        }
    }
    [rs close];
    return returnBool;
}
3.4、校验 SQL 语句合法性
/** 验证 SQL 语句是否合法
 * @param sql 待验证的SQL语句
 * 内部通过 sqlite3_prepare_v2() 来验证SQL语句,但不返回结果,而是立即调用 sqlite3_finalize()
 * @return 合法则返回 YES
 */
- (BOOL)validateSQL:(NSString*)sql error:(NSError * _Nullable __autoreleasing *)error;

4、 FMResultSet

FMResultSet:代表查询后的结果集,可以通过它遍历查询的结果集,获取出查询得到的数据!

4.1、属性
//数据库的实例
@property (nonatomic, retain, nullable) FMDatabase *parentDB;

/** 查询出该结果集所使用的SQL语句 */
@property (atomic, retain, nullable) NSString *query;

/** 将列名称 映射到 数字索引
 * 数字索引从 0 开始
 */
@property (readonly) NSMutableDictionary *columnNameToIndexMap;

/** 结果集中有多少列 */
@property (nonatomic, readonly) int columnCount;

/** 返回列名为键,列内容为值的字典。 键区分大小写  */
@property (nonatomic, readonly, nullable) NSDictionary *resultDictionary;
4.2、创建和关闭结果集
/** 使用 FMStatement 创建结果集
 * @param statement 要执行的 FMStatement
 * @param aDB 要使用的FMDatabase
 * @return 失败返回 nil
 */
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;

/** 关闭结果集
 * 通常情况下,并不需要关闭 FMResultSet,因为相关的数据库关闭时,FMResultSet 也会被自动关闭。
 */
- (void)close;
4.3、遍历结果集
/** 检索结果集的下一行。
 * 在尝试访问查询中返回的值之前,必须始终调用 -next 或 -nextWithError ,即使只期望一个值。
 * @return 检索成功返回 YES ;如果到达结果集的末端,则为 NO
 */
- (BOOL)next;
- (BOOL)nextWithError:(NSError * _Nullable __autoreleasing *)outErr;

/** 是否还有下一行数据
 * @return 如果最后一次调用 -next 成功获取另一条记录,则为' YES ';否则为 NO
 * @note 该方法必须跟随 -next 的调用。如果前面的数据库交互不是对 -next 的调用,那么该方法可能返回 NO,不管是否有下一行数据。
 */
- (BOOL)hasAnotherRow;
4.4、结果集检索信息
/** 结果集中有多少列 */
@property (nonatomic, readonly) int columnCount;

/** 指定列名称的列索引
 * @param columnName 指定列的名称
 * @return 从 0 开始的索引
 */
- (int)columnIndexForName:(NSString*)columnName;

/** 指定列索引的列名称 */
- (NSString * _Nullable)columnNameForIndex:(int)columnIdx;
4.5、根据索引获取指定值

- (int)intForColumn:(NSString*)columnName;
- (int)intForColumnIndex:(int)columnIdx;

- (long)longForColumn:(NSString*)columnName;
- (long)longForColumnIndex:(int)columnIdx;

- (long long int)longLongIntForColumn:(NSString*)columnName;
- (long long int)longLongIntForColumnIndex:(int)columnIdx;

- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx;

- (BOOL)boolForColumn:(NSString*)columnName;
- (BOOL)boolForColumnIndex:(int)columnIdx;

- (double)doubleForColumn:(NSString*)columnName;
- (double)doubleForColumnIndex:(int)columnIdx;

- (NSString * _Nullable)stringForColumn:(NSString*)columnName;
- (NSString * _Nullable)stringForColumnIndex:(int)columnIdx;

- (NSDate * _Nullable)dateForColumn:(NSString*)columnName;
- (NSDate * _Nullable)dateForColumnIndex:(int)columnIdx;

- (NSData * _Nullable)dataForColumn:(NSString*)columnName;
- (NSData * _Nullable)dataForColumnIndex:(int)columnIdx;

- (const unsigned char * _Nullable)UTF8StringForColumn:(NSString*)columnName;
- (const unsigned char * _Nullable)UTF8StringForColumnName:(NSString*)columnName __deprecated_msg("Use UTF8StringForColumn instead");
- (const unsigned char * _Nullable)UTF8StringForColumnIndex:(int)columnIdx;

/** 返回 NSNumber, NSString, NSData,或NSNull。如果该列是 NULL,则返回 [NSNull NULL]对象。
 *
 * 下述三种调用效果相同:
 *   id result = rs[0];
 *   id result = [rs objectForKeyedSubscript:0];
 *   id result = [rs objectForColumnName:0];
 *
 * 或者:
 *   id result = rs[@"employee_name"];
 *   id result = [rs objectForKeyedSubscript:@"employee_name"];
 *   id result = [rs objectForColumnName:@"employee_name"];
 */
- (id _Nullable)objectForColumn:(NSString*)columnName;
- (id _Nullable)objectForColumnName:(NSString*)columnName __deprecated_msg("Use objectForColumn instead");
- (id _Nullable)objectForColumnIndex:(int)columnIdx;
- (id _Nullable)objectForKeyedSubscript:(NSString *)columnName;
- (id _Nullable)objectAtIndexedSubscript:(int)columnIdx;

/** @warning  如果打算在遍历下一行之后或在关闭结果集之后使用这些数据,请确保首先 copy 这些数据;否则可能出现问题;
 */
- (NSData * _Nullable)dataNoCopyForColumn:(NSString *)columnName NS_RETURNS_NOT_RETAINED;
- (NSData * _Nullable)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;

/** 判断该列是否为空
 * @return 为空则返回 YES
 */
- (BOOL)columnIndexIsNull:(int)columnIdx;
- (BOOL)columnIsNull:(NSString*)columnName;
4.6、对KVC的支持

使用KVC,把数据库中的每一行数据对应到每一个对象,对象的属性要和数据库的列名保持一直。

- (void)kvcMagic:(id)object {
    int columnCount = sqlite3_column_count([_statement statement]);//用sqlite3方法取出该表的列数
    //遍历结果集
    int columnIdx = 0;
    for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
        const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);//取出该列的列名
        if (c) {//在列名不为空的情况下,使用KVC给Object的属性赋值
            NSString *s = [NSString stringWithUTF8String:c];
            [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
        }
    }
}

该方法只能给NSString类型的属性赋值 !

5、FMDatabaseQueue

FMDatabaseQueue:代表串行队列,对多线程操作提供了支持,保证多线程环境下的数据安全!

/** 数据库路径 */
@property (atomic, retain, nullable) NSString *path;

/** 开启标志 */
@property (atomic, readonly) int openFlags;

/** 自定义虚拟文件名称 */
@property (atomic, copy, nullable) NSString *vfsName;
5.1、数据库的打开与关闭
/** 根据指定的数据库路径路径实例化一个队列
 * @note 在创建数据库的时候,默认打开数据库!
 */
+ (nullable instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath;
+ (nullable instancetype)databaseQueueWithURL:(NSURL * _Nullable)url;
+ (nullable instancetype)databaseQueueWithPath:(NSString * _Nullable)aPath flags:(int)openFlags;
+ (nullable instancetype)databaseQueueWithURL:(NSURL * _Nullable)url flags:(int)openFlags;
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath;
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url;
- (nullable instancetype)initWithPath:(NSString * _Nullable)aPath flags:(int)openFlags;
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url flags:(int)openFlags;
- (nullable instancetype)initWithURL:(NSURL * _Nullable)url flags:(int)openFlags vfs:(NSString * _Nullable)vfsName;
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName{
    self = [super init];
    if (self != nil) {
        _db = [[[self class] databaseClass] databaseWithPath:aPath];
        FMDBRetain(_db);
        //在创建数据库的时候,默认打开数据库!
        BOOL success = [_db openWithFlags:openFlags vfs:vfsName];
        if (!success) {
            NSLog(@"Could not create database queue for path %@", aPath);
            FMDBRelease(self);
            return 0x00;
        }
        
        _path = FMDBReturnRetained(aPath);
        //GCD 的串行队列
        _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
        //为 _queue 设置标识数据:指定的键,值为 self 的地址
        dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
        _openFlags = openFlags;
        _vfsName = [vfsName copy];
    }
    return self;
}

/** 获取 FMDatabase 类,用于实例化数据库对象。 */
+ (Class)databaseClass {
    return [FMDatabase class];
}

/** 关闭数据库  */
- (void)close {
    FMDBRetain(self);
    dispatch_sync(_queue, ^() {
        [self->_db close];
        FMDBRelease(_db);
        self->_db = 0x00;
    });
    FMDBRelease(self);
}

/** 中断数据库操作  */
- (void)interrupt {
    [[self database] interrupt];
}

注意:由于 FMDatabaseQueue 的任务执行,都是在串行队列 dispatch_queue 中同步执行!为防止线程死锁,任何情况下都不能在同一个 FMDatabaseQueue 实例下嵌套任务!

5.2、将数据库操作分派到队列
/** 在队列上同步执行数据库操作
 * @param block 要在 FMDatabaseQueue 队列上运行的代码
 */
- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {
#ifndef NDEBUG
    //断言:确保 inDatabase: 不会套用造成死锁
    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);
    dispatch_sync(_queue, ^() {//同步执行串行队列
        FMDatabase *db = [self database];
        block(db);
        if ([db hasOpenResultSets]) {
            NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
        }
    });
    FMDBRelease(self);
}

在创建 FMDatabaseQueue 时,设定了 dispatch_queue 的标识数据,此时通过指定的键可以取出对应的值:

  • 如果是主队列、或者全局并发队列,取出的 NULL ,不影响代码执行;
  • 如果是在其它 FMDatabaseQueue 对象的dispatch_queue 中执行,则返回的是其它的实例,不影响代码执行;
  • 如果是在当前 FMDatabaseQueue 对象的dispatch_queue 中执行,则会造成 dispatch_queue 的死锁,因此此处断言 assert(currentSyncQueue!=self) !
5.3、将事务分派到队列
/** 使用事务在 串行dispatch_queue 同步执行数据库操作
 * @note 没有开辟分线程,在当前线程中操作!
 * @note 不可以嵌套使用,否则造成线程死锁;
 */
- (void)beginTransaction:(FMDBTransaction)transaction withBlock:(void (^)(FMDatabase *db, BOOL *rollback))block {
    FMDBRetain(self);
    dispatch_sync(_queue, ^() { //同步执行串行队列
        BOOL shouldRollback = NO;
        switch (transaction) {
            case FMDBTransactionExclusive:
                [[self database] beginTransaction];//默认开始互斥事务
                break;
            case FMDBTransactionDeferred:
                [[self database] beginDeferredTransaction];//开始一个延迟事务
                break;
            case FMDBTransactionImmediate:
                [[self database] beginImmediateTransaction];//立即开启事务
                break;
        }
        block([self database], &shouldRollback);
        if (shouldRollback) {
            [[self database] rollback];
        }else {
            [[self database] commit];
        }
    });
    FMDBRelease(self);
}

/**默认开始互斥事务  */
- (void)inTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:FMDBTransactionExclusive withBlock:block];
}

/** 使用延迟事务在队列上同步执行数据库操作  */
- (void)inDeferredTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:FMDBTransactionDeferred withBlock:block];
}

/** 使用互斥事务在队列上同步执行数据库操作。 */
- (void)inExclusiveTransaction:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block {
    [self beginTransaction:FMDBTransactionExclusive withBlock:block];
}

/** 使用事务立即在队列上同步执行数据库操作。*/
- (void)inImmediateTransaction:(__attribute__((noescape)) void (^)(FMDatabase * _Nonnull, BOOL * _Nonnull))block {
    [self beginTransaction:FMDBTransactionImmediate withBlock:block];
}


/** 使用 保存点机制 同步执行数据库操作
 *
 * @note  不能嵌套操作,因为调用它会从池中拉出另一个数据库,会得到一个死锁。
 *        如果需要嵌套,用 FMDatabase 的 -startSavePointWithName:error: 代替。
 */
- (NSError * _Nullable)inSavePoint:(__attribute__((noescape)) void (^)(FMDatabase *db, BOOL *rollback))block{
    static unsigned long savePointIdx = 0;
    __block NSError *err = 0x00;
    FMDBRetain(self);
    dispatch_sync(_queue, ^() { 
        NSString *name = [NSString stringWithFormat:@"savePoint%ld", savePointIdx++];
        BOOL shouldRollback = NO;
        if ([[self database] startSavePointWithName:name error:&err]) {// 设置保存点
            block([self database], &shouldRollback);
            if (shouldRollback) {//如果需要回滚
                //回滚到保存点
                [[self database] rollbackToSavePointWithName:name error:&err];
            }
            //刪除保存点
            [[self database] releaseSavePointWithName:name error:&err];
        }
    });
    FMDBRelease(self);
    return err;
}


/** 手动配置 WAL 检查点
 * @param name sqlite3_wal_checkpoint_v2() 的数据库名称
 * @param checkpointMode  sqlite3_wal_checkpoint_v2() 的检查点类型
 * @param logFrameCount 如果不为NULL,则将其设置为日志文件中的总帧数,如果检查点由于错误或数据库没有处于WAL模式而无法运行,则将其设置为-1。
 * @param checkpointCount 如果不是NULL,那么这将检查点帧总数日志文件(包括检查点之前任何已经调用的函数),如果检查点由于错误或数据库不在WAL模式下而无法运行,则为-1。
 * @return 成功返回 YES;
 */
- (BOOL)checkpoint:(FMDBCheckpointMode)mode error:(NSError * __autoreleasing *)error{
    return [self checkpoint:mode name:nil logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(FMDBCheckpointMode)mode name:(NSString *)name error:(NSError * __autoreleasing *)error{
    return [self checkpoint:mode name:name logFrameCount:NULL checkpointCount:NULL error:error];
}
- (BOOL)checkpoint:(FMDBCheckpointMode)mode name:(NSString *)name logFrameCount:(int * _Nullable)logFrameCount checkpointCount:(int * _Nullable)checkpointCount error:(NSError * __autoreleasing _Nullable * _Nullable)error{
    __block BOOL result;
    __block NSError *blockError;
    
    FMDBRetain(self);
    dispatch_sync(_queue, ^() {
        result = [self.database checkpoint:mode name:name logFrameCount:logFrameCount checkpointCount:checkpointCount error:&blockError];
    });
    FMDBRelease(self);
    
    if (error) {
        *error = blockError;
    }
    return result;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249