FMDB源码解析与使用

介绍

FMDB是一个基于SQLite的数据库框架;使用OC语言对SQLite3的C语言接口做了一层面向对象的封装;框架代码本身比较简单,对比苹果自身的数据库框架Core Data来说,更加的轻量级和灵活;

FMDB封装了操作数据库可能会使用到的一些接口,包括最基本的CURD,以及事物和保存点等;并通过一个GCD的Serial队列保证在多线程环境下的数据安全;

源码解析

FMDB本身的代码不是很复杂,主要包括以下几个组成文件:

  • 1、FMDatabase:它与数据库文件是一一对应的,在新建一个FMDatabase对象时,可以关联一个已有的数据库文件;在对象中封装了操作数据库的基本接口,如增删改查等;

  • 2、FMDatabaseAdditions:是FMDatabase的一个Category分类,封装了一些数据库的便捷功能;如验证SQL语句的正确性、判断一个表或者是表中的某一行是否存在等;

  • 3、FMDatabaseQueue:当需要在多线程环境下执行数据库操作时,就需要通过FMDatabaseQueue初始化数据库对象,通过FMDatabaseQueue的接口执行数据库操作能保证多线程环境下的访问安全;

  • 4、FMDatabasePool:一个FMDatabase的对象池封装,在多线程环境中访问单个FMDatabase对象是很容易引起问题的,可以通过FMDatabasePool对象池来解决多线程下的访问安全问题;但是FMDB建议我们优先使用FMDatabaseQueue;除非确实不得已才去使用FMDatabasePool,在此情况下还需要注意避免使用时产生死锁问题;

  • 5、FMResultSet:封装了select语句的结果集,可以通过它遍历查询的结果集,获取出查询得到的数据;

内部实现与框架的基本使用

我们主要看一下FMDatabaseFMDatabaseQueueFMResultSet这三个类的关键实现;

先从FMDatabase开始,在这个类的.h文件介绍说明中有如下描述:

An `FMDatabase` is created with a path to a SQLite database file.  This path can be one of these three:

 1. A file system path.  The file does not have to exist on disk.  If it does not exist, it is created for you.
 2. An empty string (`@""`).  An empty database is created at a temporary location.  This database is deleted with the `FMDatabase` connection is closed.
 3. `nil`.  An in-memory database is created.  This database will be destroyed with the `FMDatabase` connection is closed.

 For example, to create/open a database in your Mac OS X `tmp` folder:

    FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];

 Or, in iOS, you might open a database in the app's `Documents` directory:

    NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
    NSString *dbPath   = [docsPath stringByAppendingPathComponent:@"test.db"];
    FMDatabase *db     = [FMDatabase databaseWithPath:dbPath];

 (For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [http://www.sqlite.org/inmemorydb.html](http://www.sqlite.org/inmemorydb.html))

通过以上介绍我们知道,FMDatabase是通过一个SQLite数据库文件的路径创建出来的对象;文件的路径可以是以下三种:
1、一个具体的文件路径;如果路径上没有对应的文件,系统会帮我们在路径上创建一个文件;
2、如果路径是@""这样的空字符串,系统会在临时文件夹内创建一个空数据库,当使用完成关闭数据库连接时,这个临时文件会被删除;
3、如果路径为nil则会在内存中创建一个数据库,当数据库连接关闭时,内存中的数据库会被删除;

以下是FMDatabase的初始化方法:

- (instancetype)initWithPath:(NSString *)path {
    //确保在开始前sqlite3是线程安全的状态
    assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do.
    
    self = [super init];
    
    if (self) {
        _databasePath               = [path copy];  //记录书库的路径
        _openResultSets             = [[NSMutableSet alloc] init];  //存放查询语句结果集的集合对象
        _db                         = nil;  //数据库句柄,当调用open函数后,这个指针会被赋值,指向数据库对象
        _logsErrors                 = YES;  //设置数据库操作出错时,打印日志信息
        _crashOnErrors              = NO;  //关闭访问出错让程序退出的开关
        _maxBusyRetryTimeInterval   = 2;   //当数据库被上锁后,每隔2秒后重新尝试访问
    }
    
    return self;
}

初始化方法内只对一些基础变量进行了赋值,并没有涉及到数据库的操作,初始化完成后可以通openopenWithFlags: vfs:方法打开数据库;数据库操作结束后,通过调用close方法关闭数据库连接;

在FMDatabase中还封装了一个FMStatement内部类,这个类主要用在框架内部,用于把一个SQL语句封装成一个对象;

在FMDatabase中主要封装了两种类型的数据库操作方法,分别是executeUpdateexecuteQuery系列,其中executeUpdate系方法用于数据的增、删、改操作,executeQuery系方法用于查询操作;数据库的事物保存点等操作也是通过调用executeUpdate系方法实现的;

/** 以下接收不同类型的参数输入,并最终执行一条SQL语句,完成增、删、改等操作 */
- (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError * _Nullable *)outErr, ...;
- (BOOL)executeUpdate:(NSString*)sql, ...;
- (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2);
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
- (BOOL)executeUpdate:(NSString*)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments;
- (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args;


/** 以下方法执行多条SQL语句;参数sql是通过“;”拼接起来的多条语句组合而成的字符串  */
- (BOOL)executeStatements:(NSString *)sql;
- (BOOL)executeStatements:(NSString *)sql withResultBlock:(__attribute__((noescape)) FMDBExecuteStatementsCallbackBlock _Nullable)block;


/** 以下接收不同类型的参数输入,并执行一条SQL查询语句返回结果集 */
- (FMResultSet * _Nullable)executeQuery:(NSString*)sql, ...;
- (FMResultSet * _Nullable)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2);
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql values:(NSArray * _Nullable)values error:(NSError * _Nullable __autoreleasing *)error;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary * _Nullable)arguments;
- (FMResultSet * _Nullable)executeQuery:(NSString *)sql withVAList:(va_list)args;


/** 事物相关的接口 */
- (BOOL)beginTransaction;  //Begin a transaction 
- (BOOL)beginDeferredTransaction;  //Begin a deferred transaction
- (BOOL)commit;  //Commit a transaction  
- (BOOL)rollback;  //Rollback a transaction

更新与查询的接口几乎是一一对应的,都是接收一条SQL语句然后执行并得到执行结果;由于SQL语句中会使用“?”做参数的占位符,然后根据传入的参数补充出完整的SQL语句,不同的方法提供了不同的方式接受SQL语句的参数传入;下面看一个调用的例子:

  //创建数据库对象
  FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
  if (![db open]) {  //打开数据库
      NSLog(@"Could not open db.");
      return 0;
  }
  //通过executeUpdate语句创建一张新的表
  [db executeUpdate:@"create table test (a text, b text, c integer, d double, e double)"];
    
  [db beginTransaction];  //开始一个事物,即通过使用事物的方式更新数据库的数据
  int i = 0;
  while (i++ < 20) {
      //在数据库中插入数据
      [db executeUpdate:@"insert into test (a, b, c, d, e) values (?, ?, ?, ?, ?)" ,
          @"hi'", // look!  I put in a ', and I'm not escaping it!
          [NSString stringWithFormat:@"number %d", i],
          [NSNumber numberWithInt:i],
          [NSDate date],
          [NSNumber numberWithFloat:2.2f]];
  }
  [db commit];  //提交一个事物

executeUpdate方法内部首先会通过调用sqlite3_prepare_v2函数执行传入的sql语句参数并生成一个sqlite3_stmt *pStmt指针;然后根据不同的executeUpdate方法解析传递给sql语句的参数并把参数绑定到pStmt对象中,在通过sqlite3_step(pStmt)函数执行处理好的SQL语句,如果是查询语句会同时返回查询的结果集;中间如果有任何环节出现错误都会通过sqlite3_finalize(pStmt)语句释放这个预备好的SQL语句;

在FMDatabase的executeQuery系的方法内部,会把select语句的结果集通过FMResultSet对象进行处理;看一下掉用到FMResultSet对象的过程:

    // the statement gets closed in rs's dealloc or [rs close];
    rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];  //rs是FMResultSet类型的对象
    [rs setQuery:sql];
    
    NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
    [_openResultSets addObject:openResultSet];  //在FMDatabase中缓存查询后的结果集

以上代码的statement参数是FMStatement的对象,在处理好SQL语句后会生成statement对象,然后把对象传递给FMResultSet方法内部,去执行查询操作并得到查询的结果;看一下FMResultSet的内部方法实现:

+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
    
    FMResultSet *rs = [[FMResultSet alloc] init];  //生成结果集对象
    [rs setStatement:statement];  //设置好SQL语句
    [rs setParentDB:aDB];  //设置好数据库句柄
    
    NSParameterAssert(![statement inUse]);
    [statement setInUse:YES]; // weak reference
    
    return FMDBReturnAutoreleased(rs);
}

方法把数据库的句柄和拼接好的SQL语句都传递到了FMResultSet对象中;之后可以通过调用FMResultSet对象的next方法遍历查询结果集,在next方法的内部通过调用sqlite3_step([_statement statement])执行这个查询语句;通过FMResultSet遍历结果集的调用过程如下:

  //通过执行executeQuery语句,返回FMResultSet对象(查询结果集)
  FMResultSet *rs = [db executeQuery:@"select rowid,* from test where a = ?", @"hi'"];  
    while ([rs next]) {  //调用next方法逐行便利结果集,并打印每一行的数据
        // just print out what we've got in a number of formats.
        NSLog(@"%d %@ %@ %@ %@ %f %f",
              [rs intForColumn:@"c"],
              [rs stringForColumn:@"b"],
              [rs stringForColumn:@"a"],
              [rs stringForColumn:@"rowid"],
              [rs dateForColumn:@"d"],
              [rs doubleForColumn:@"d"],
              [rs doubleForColumn:@"e"]);
    }
    // close the result set.
    // it'll also close when it's dealloc'd, but we're closing the database before
    // the autorelease pool closes, so sqlite will complain about it.
    [rs close];   //对象被dealloc之前,先通过close方法关闭和这个sql语句有关的资源

以上代码通过FMResultSet对象遍历了select语句的结果集,其中不同类型的column数据读取在FMResultSet中都有对应的取值方法;在FMResultSet中还包括一些其他的便捷方法,如通过列的index获取columnName,或者通过columnName获取列的index等;

线程安全

在FMDB框架中,除了有基本的数据库操作外,还提供了多线程环境下的安全访问机制;我们看一下FMDatabaseQueue中是如何保证多线程下的安全访问的;

先看一下FMDatabaseQueue这个类的“.h”文件说明:

 To perform queries and updates on multiple threads, you'll want to use `FMDatabaseQueue`.

 Using a single instance of `<FMDatabase>` from multiple threads at once is a bad idea.  It has always been OK to make a `<FMDatabase>` object *per thread*.  Just don't share a single instance across threads, and definitely not across multiple threads at the same time.

 Instead, use `FMDatabaseQueue`. Here's how to use it:

    First, make your queue.
    FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

 Then use it like so:

    [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]) {
            //…
        }
    }];

   An easy way to wrap things up in a transaction can be done like this:

    [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]];
    }];

 `FMDatabaseQueue` will run the blocks on a serialized queue (hence the name of the class).  So if you call `FMDatabaseQueue`'s methods from multiple threads at the same time, they will be executed in the order they are received.  This way queries and updates won't step on each other's toes, and every one is happy.

以上说明的大概意思是说,FMDatabase对象只适合在单线程per thread中使用,再多线程下是不安全的;多线程环境下应该使用FMDatabaseQueue对象替代,以保证数据库的安全访问;

使用FMDatabaseQueue进行数据库操作时,会把对应的操作封装成block的形式添加到一个serial的线程队列中,以保证所有异步添加进来的数据库操作都会在serial队列中按顺序执行,保证多线程环境下的数据安全;

也就是在FMDatabaseQueue中的多线程,实际上只停留在调用[queue inDatabase:]等添加数据库操作的API上;在执行添加进来的操作时,是通过与serial队列对应的单线程完成的,通过这种方式保证在执行操作时的数据同步与访问安全;

我们看一下FMDatabaseQueue的初始化和添加block操作的内部实现:

//初始化方法
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
    self = [super init];
    
    if (self != nil) {
        //初始化数据库对象
        _db = [[[self class] databaseClass] databaseWithPath:aPath];
        FMDBRetain(_db);

//根据SQLite的版本信息,调用不同的数据库打开方法
#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);
        //生成执行数据库操作的serial队列
        _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
        //设置这个queue的关联key/value标志信息,后面可以通过这个key在queue中获取出关联的self对象,用于判断是否会产生死锁
        dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
        _openFlags = openFlags;
        _vfsName = [vfsName copy];
    }
    
    return self;
}


//添加block操作到FMDatabaseQueue中的方法
- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {
#ifndef NDEBUG
    /* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
     * and then check it against self to make sure we're not about to deadlock. */
    //根据key信息在当前执行queue中取出对象,大多数时候取出来的应该是nil才对,这里接着会判断是否会产生死锁
    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);
    //在serial队列中同步执行传递进来的数据库操作block
    dispatch_sync(_queue, ^() {
        FMDatabase *db = [self database];
        block(db);  //执行block操作
        
        if ([db hasOpenResultSets]) {
            NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
//debug环境下打印一些查询的数据日志      
#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);
}

通过以上实现可以看到在初始化方法中生成了一个serial queue队列,执行操作主要是通过serial queuedispatch_sync()同步操作API完成的,这样就避免了访问数据时异步操作的可能性,从而保证多线程下的安全访问;

需要补充说明的是在初始化时通过调用dispatch_queue_set_specific ()函数,设置了这个queue的关联标识对象;然后在执行添加的block操作时,首先通过dispatch_get_specific方法在queue中取出标识对象,用来判断是否与当前的self对象相同,如果相同就意味着当前执行inDatabase方法的队列与方法内的dispatch_sync(_queue, ...);中的_queue是同一个队列,在这种情况下会产生死锁,因此需要抛出程序异常;这里需要对GCD的API有一些基础理解,关于GCD的理解可以看一下这篇文章

最后再看一个FMDatabaseQueue的实际使用代码:

  //新建FMDatabaseQueue对象
  FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:dbPath];
  //通过inDatabase接口安全的执行数据库操作
  [queue inDatabase:^(FMDatabase *adb) {
        //在queue中执行create、insert等操作
        [adb executeUpdate:@"create table qfoo (foo text)"];
        [adb executeUpdate:@"insert into qfoo values ('hi')"];
        [adb executeUpdate:@"insert into qfoo values ('hello')"];
        [adb executeUpdate:@"insert into qfoo values ('not')"];
            
        //执行查询操作
        int count = 0;
        FMResultSet *rsl = [adb executeQuery:@"select * from qfoo where foo like 'h%'"];
            while ([rsl next]) {
                count++;
            }
   }];

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,386评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,939评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,851评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,953评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,971评论 5 369
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,784评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,126评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,765评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,148评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,744评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,858评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,479评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,080评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,053评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,278评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,245评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,590评论 2 343

推荐阅读更多精彩内容