22 Mar 2017

FMDB 代码阅读

FMDB 是iOS平台的SQLite数据库框架,以ObjC的方式封装了SQLite的C语言的API。FMDB使用起來更加的面向对象,省去了很多麻烦、冗余的C语言代码。相比Apple自带的Core Data框架,更加的轻量和灵活。提供了多线程安全的数据库操作的方法,有效的防止数据混乱。

项目文件

  • FMDatabase : 一个SQLite数据库操作单例,通过它可以对数据库进行增删改查等操作。

  • FMResultSet : FMDatabase执行查询之后的结果集。

  • FMDatabaseAdditions : 拓展FMDatabase类,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等功能。

  • FMDatabaseQueue : 使用串行对列 ,操作多线程。

  • FMDatabasePool : 使用任务池的形式,操作多线程。

FMDatabase

打开数据库连接

  • -(BOOL)open; 其实是对sqlite3_open()函数的封装。

  • - (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout; 设置重试时间。其实调用的是 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会不断用用该回调函数,直到其他线程或者进程释放锁。获得锁之后,不会再调用该回调函数,从而继续向下执行下去,进行数据库操作。该函数是在获取不到锁的时候,以执行回调函数的次数來进行延时,等待其他进程或者线程操作数据库结束,从而获得锁进行操作数据库。

查询数据库

executeQuery 系列函数从根本上看,其实调用的都是

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args

  • 参数sql: 需要查詢的sql语句。

  • 参数arrayArgs: 数组类型的参数。

FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > ?" withArgumentsInArray:@[@20]];
  • 参数dictionaryArgs: 字典类型的参数。
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > :age" withParameterDictionary:@{@"age":@20}];
  • 参数args: 可变参数类型。
FMResultSet *resultSet = [_db executeQuery:@"SELECT * FROM t_student WHERE age > ?",@(20)];

更新数据库操作

这并不只是单单更新数据,而是对数据库有更改的操作,增删改都算。FMDB调用的都是executeupdate系列函数。这个函数基本上跟executeQuery系列函数的实现基本相同。只是它生成statement对象后,直接调用rc = sqlite3_step(pStmt);更新执行,而没有像executeQuery延迟到FMResultSet中的next函数中执行。

一次性执行多条sql语句。

使用executeStatements函数可以一次性执行多条sql语句。其实现方式就是对sqlite3_exec函数的封装。

FMDB的加解密

FMDataase中使用- (BOOL)setKey:(NSString*)key;- (BOOL)setKeyWithData:(NSData *)keyData;输入数据库密码以求验证用户身份,使用- (BOOL)rekey:(NSString*)key;- (BOOL)rekeyWithData:(NSData *)keyData;来给数据库设置密码或者清除密码。这两类函数分別对sqlite3_keysqlite3_rekey函数进行了封装。

FMDatabaseAdditions

XXXForQuery系列函数

对查询结果只有一个值的情况进行优化,有多个值也只取第一个值。

/**
 *  使用FMDatabaseAdditions中的intForQuery函数查找数据,如果返回结果有多个数据只取第一条数据
 */
- (void)queryForIntForQuery{
    int idx = [_db intForQuery:@"SELECT id FROM t_student WHERE age = ?",@(26)];
}

数据库的一些概要信息

-(BOOL)tableExists:(NSString*)tableName;数据库表是否存在。

-(BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName;在tableName表中columnName是否存在。

-(FMResultSet*)getSchema;数据库的一些概要信息。

校验sql语句是否合法

-(BOOL)validateSQL:(NSString *)sql error:(NSError **)error;

FMResultSet

初始化对象

  • 参数1:(FMStatement *)statement

该对象主要是对sqlite3_stmt的封装,sqlite3_stmt * 所表示的内容可以看成是预处理过的sql语句,已经不是我们熟知的sql语句。它是一个已经把sql语句解析了,用sqlite自己表示记录的内部数据结构。

  • 参数2:(FMDatabase *)aDB 该结果集所属于的FMDatabase数据库操作对象。
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;

遍历取得所有的结果集合

-(BOOL)next; 其实是对 -(BOOL)nextWithError:(NSError **)outErr; 函数的封装。主要作用是通过sqlite3_step函数对FMStatement中的sqlite3_stmt对象进行逐行取值。

列名与该列的列数的一一对应关系

  • @property (readonly) NSMutableDictionary *columnNameToIndexMap; 对象中保存了列名与索引一一对应的关系的对照表。

  • -(int)columnIndexForName:(NSString *)columnName; 根据列名获取该列所在第几列(列的索引)

  • -(NSString *)columnNameForIndex:(int)columnIdx; 根据列的索引获取该列的名称。

获得每一行中每一个列字段的值。

  • -XXXForColumnIndex:(int)columnIdx; 根据列的索引获取该列的值。

  • -XXXForColumn:(NSString*)columnName; 根据列的名称获取该列的值。

  • -XXXForColumnIndex:(int)columnIdx; 其实是对sqlite3column*函数的封装。

- (int)intForColumnIndex:(int)columnIdx {
    return sqlite3_column_int([_statement statement], columnIdx);
}

获取每一行中所有的结果集合

- (NSDictionary*)resultDictionary;

FMDatabaseQueue

使用实例:

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];

    FMResultSet *rs = [db executeQuery:@"select * from foo"];
    while ([rs next]) {
        …
    }
}];

[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];

    if (whoopsSomethingWrongHappened) {
        *rollback = YES;
        return;
    }
    // etc…
    [db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];

事务的实现

数据库中的事务也是保证数据库安全的一种手段。一段sql语句,要么全部成功,要么全部不成功。

  • 关于延时性事务和独占性事务的区别

在SQLite 3.0.8或更高版本中,事务可以是延迟的,即时的或者独占的。“延迟的”即是说在数据库第一次被访问之前不获得锁。这样就会延迟事务,BEGIN语句本身不做任何事情。直到初次读取或访问数据库时才获取锁。对数据库的初次读取创建一个SHARED锁,初次写入创建一个RESERVED锁。由于锁的获取被延迟到第一次需要时,别的线程或进程可以在当前线程执行BEGIN语句之后创建另外的事务写入数据库。若事务是即时的,则执行BEGIN命令后立即获取RESERVED锁,而不等数据库被使用。在执行BEGIN IMMEDIATE之后, 你可以确保其它的线程或进程不能写入数据库或执行BEGIN IMMEDIATE或BEGIN EXCLUSIVE. 但其它进程可以获取数据库。 独占事务在所有的数据库获取EXCLUSIVE锁,在执行BEGIN EXCLUSIVE之后,你可以确保在当前事务结束前没有任何其它线程或进程能够读写数据库。

FMDatabasePool

FMDatabasePool : 使用任务池的形式,对多线程的操作提供支持。

不过官方对这种方式并不推荐使用(ONLY_USE_THE_POOL_IF_YOU_ARE_DOING_READS_OTHERWISE_YOULL_DEADLOCK_USE_FMDATABASEQUEUE_INSTEAD),优先选择FMDatabaseQueue的方式。