FMDB详解

一、什么是FMDB
FMDB是iOS平台用OC语言封装了SQLite API的数据库框架    

二、FMDB的优点

1、面向对象,使用了oc语言,避免了和c打交道
2、FMDB是轻量级框架,使用更加灵活、方便
3、提供了多线程的方式来操作数据库,可以防止多线程操作带来的数据混乱


三、 FMDB常用的类

1、FMDatabase 
sqlite的数据库对象,一个FMDatabase对象就代表了一个单独的sqlite数据库连接,用来执行SQLite的命令。

2、FMResultSet
sqlite的结果集,数据库查询结果集

3、FMDatabaseQueue
多线程,使用多线程对数据进行操作,是线程安全的。

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

5、FMDatabasePool
可以使用任务池的方式,对多线程的操作提供了支持


四、 数据库的执行操作的方法
一般数据库的执行的过程:




1、  使用sqlite3_open打开数据库连接
2、  使用sqlite3_prepare_v2或相关的函数创建这个对象
3、  使用sqlite3_bind_*()给宿主参数(host parameters)绑定值
4、  通过调用sqlite3_step一次或多次来执行这个sql
5、  使用sqlite3_reset()重置这个语句,然后回到第3步,这个过程做0次或多次
6、  使用sqlite3_finalize()销毁这个对象,回到第2步。
7、  使用sqlite3_close()关闭这个数据库连接。


1、 open方法 ---- sqlite3_open(const char *filename, sqlite3 **ppDb)


该方法是打开一个指向SQLite数据库文件的连接,返回一个SQLite程序的数据库连接对象。


  注意:
 a、如果文件名 filename 参数是 NULL 或者 ":memory:",那么 sqlite3_open() 将会在 RAM 中创建一个内存数据库,该数据库只会在 session 的有效时间内持续。
 b、如果文件名 filename 参数是 空字符串(@""),那么会在临时目录创建一个空的数据库,当链接关闭时,文件也被删除。
 c、如果文件名 filename参数 不是 NULL,那么 sqlite3_open() 将使用filename的值去打开数据库文件。如果该文件名称的文件不存在,sqlite3_open() 将创建一个新的命名为该名称的数据库文件并打开。

2、 prepare方法 ----  sqlite3_prepare_v2(sqlite3 *db,const char *zSql,    int nByte, sqlite3_stmt **ppStmt, const char **pzTail);
sqlite3_prepare_v2()编译SQL语句生成VDBE执行码。这个接口需要一个数据库连接指针以及一个要准备的包含SQL语句的文本。它实际上并不执行这个SQL语句,它仅仅为执行准备这个sql语句

3、bind方法 ----  sqlite3_bind_double(sqlite3_stmt*, int, double);
绑定变量是为了提高解析sql语句的效率,比如你要执行很多一样的语句,比如
select ppp,bbb from jjj where ddd=aaa;
如果经常通过改变aaa这个谓词赋值来查询,比如
select ppp,bbb from jjj where ddd=ccc;
select ppp,bbb from jjj where ddd=eee;
这样每条语句都要被数据库解析一次,这样比较浪费资源,而且耗时,如果把aaa换成“:1”这样的绑定变量形式,无论ddd后面是什么值,都不需要重复解析,这样节省资源和节省时间。

4、step方法 ---- int sqlite3_step(sqlite3_stmt*);
这个过程用于执行有前面sqlite3_prepare创建的准备语句。
 
5、reset方法 ---- sqlite3_reset(sqlite3_stmt *pStmt);
重置所有绑定的值,回到调用sqlite3_prepare后的状态。

6、finalize方法  ----  sqlite3_finalize(sqlite3_stmt *pStmt);
这个过程销毁前面被sqlite3_prepare创建的准备语句,每个准备语句都必须使用这个函数去销毁以防止内存泄露


7、close方法 ---- sqlite3_close(sqlite3*)
关闭之前打开的数据库,而且所有和连接数据有关的语句都应该在执行关闭数据库语句之前执行完,如果还有查询的语句没有执行完毕,这时会返回禁止关闭的错误消息(SQLITE_BUSY),第一个参数的是数据库对象


8、exec方法 ---- sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)
该方法执行一个快捷的sql命令、它实际上是将编译,执行进行了封装,等价于 sqlite3_prepare_v2(), sqlite3_step()和sqlite3_finalize()一起的功能。
sqlite3_exec可以执行任何sql语句,包括事务("BEGIN TRANSACTION")、回滚("ROLLBACK")和提交("COMMIT")等语句。


五、 FMDB的使用方法
  1、创建FMDB
 注意:这里有个文件路径有三种形式 具体可以看一下上面的sqlite3_open方法
 a、具体的文件路径
 会根据具体的文件路径来创建相应的数据库,如果路径不存在,会自动创建
 b、路径为空的字符串@“”
 会在临时目录下面创建一个空的数据库,当FMDB连接关闭时,数据库文件也被删除。
 c、当为nil的时候
 会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

    _dbPath = @"XXXXX.sqlite";
    _db = [[FMDatabase alloc] initWithPath:_dbPath];
    [_db open];
    [_db executeStatements:@"CREATE TABLE IF NOT EXISTS  FMDBTABLE (key TEXT, attr1 TEXT)"];

打开数据库的源码解析
[_db open]的代码解析


- (BOOL)open {
    if (_db) {
        return YES;
    }
    int err = sqlite3_open([self sqlitePath], (sqlite3**)&_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 executeStatements:@"CREATE TABLE IF NOT EXISTS  FMDBTABLE (key TEXT, attr1 TEXT)"]的代码解析
    int rc;
    char *errmsg = nil;
    rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg);
    if (errmsg && [self logsErrors]) {
        NSLog(@"Error inserting batch: %s", errmsg);
        sqlite3_free(errmsg);
    }
    return (rc == SQLITE_OK);

其实创建数据库就是使用的是c语言的这个的api通过sql语句来创建数据库对象
SQLITE_API int sqlite3_exec(
  sqlite3*,         /* An open database */
  const char *sql,  /* SQL to be evaluated */
  int (*callback)(void*,int,char**,char**),  /* Callback function */
  void *,               /* 1st argument to callback */
  char **errmsg        /* Error msg written here */
);


2、插入数据
在FMDB里面,除了查询以外的其他所有操作,都叫做“更新”
   [_db executeUpdate:@"INSERT INTO FMDBTABLE (key, attr1) VALUES (?,?)",key,attr1];

插入数据的源码解析
第一步、先判断数据库是否存在
- (BOOL)databaseExists {
    if (!_db) {
        NSLog(@"The FMDatabase %@ is not open.", self);  
#ifndef NS_BLOCK_ASSERTIONS
        if (_crashOnErrors) {
            NSAssert(false, @"The FMDatabase %@ is not open.", self);
            abort();
        }
#endif  
        return NO;
    }
    return YES;
}
第二步、判断数据库是否正在使用、做操作,如果正在使用就返回no,这个是避免同个数据库被多个地方使用,造成数据错乱。

    if (_isExecutingStatement) {
        [self warnInUse];
        return NO;
    }

第三步、再在我们的操作的语句集合里面查找是否之前使用过该语句,如果使用过创建成相应的查询语句对象,如果不存在将sql文本转换成一个准备语句对象。这样做是为了提高sql执行的效率。

    if (_shouldCacheStatements) {
        cachedStmt = [self cachedStatementForQuery:sql];
        pStmt = cachedStmt ? [cachedStmt statement] : 0x00;
        [cachedStmt 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();
            }
            if (outErr) {
                *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]];
            }  
            sqlite3_finalize(pStmt);
            _isExecutingStatement = NO;
            return NO;
        }
    }
 
第四步、先判断是否带有参数,如果带有参数,逐个根据我们的sql对象的不同参数类型,调用不同的方法来进行值绑定。

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

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



第五步、执行
 
     rc      = sqlite3_step(pStmt);


第六步、做一些错误的解析
第七步、把我们刚刚执行的sql语句存入到语句集合中,并将使用的次数加1

    if (_shouldCacheStatements && !cachedStmt) {
        cachedStmt = [[FMStatement alloc] init];
        [cachedStmt setStatement:pStmt];
        [self setCachedStatement:cachedStmt forQuery:sql];
        FMDBRelease(cachedStmt);
    }
    int closeErrorCode;
    if (cachedStmt) {
        [cachedStmt setUseCount:[cachedStmt useCount] + 1];
        closeErrorCode = sqlite3_reset(pStmt);
    }
    else {
        closeErrorCode = sqlite3_finalize(pStmt);
    }
- (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query {
    query = [query copy]; // in case we got handed in a mutable string...
    [statement setQuery:query];
    NSMutableSet* statements = [_cachedStatements objectForKey:query];
    if (!statements) {
        statements = [NSMutableSet set];
    }
    [statements addObject:statement];
    [_cachedStatements setObject:statements forKey:query];
    FMDBRelease(query);
}
3、查找数据


    NSString *SQL = [NSString stringWithFormat: @"SELECT * FROM FMDBTABLE WHERE key = %@", key];
    FMResultSet *s = [_db executeQuery:SQL];
    return s;



查找数据的源码
第一步、第二步、第三步、第四步、第五步、第六步、第七步和之前插入数据的方式是一样的
第七步、创建结果集,获取查找的数据

    rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
    [rs setQuery:sql];
    
    NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
    [_openResultSets addObject:openResultSet];


FMResultSet常用的方法如下图:


4、更新数据

    [_db executeUpdate:@"UPDATE FMDBTABLE SET attr1 = 5"];

更新数据的源码和插入数据的源码一样,可以参考插入数据的源码
###多线程


1、使用多线程进行操作

    [_queue inDatabase:^(FMDatabase*db) {
        [db open];
        //插入记录到表中
        NSString *sqlStr = @"insert into MYTAble(num,name,sex) values(4,'xiaoming','m')";
        BOOL result = [db executeUpdate:sqlStr];
        if (!result) {
            NSLog(@"多线程  插入失败");
            NSLog(@"---%@",[NSThread currentThread]);
            [db close];
        }else{
            NSLog(@"多线程  插入成功");
            NSLog(@"---%@",[NSThread currentThread]);
        }
    }];

使用多线程进行操作的源码解析
主要就是FMDB不能多线程使用同一个实例,其实就是开启的是串行队列,顺序执行相应的操作,这样避免了多线程同时访问数据库。

    #ifndef NDEBUG
        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:]");       
    #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);



###使用事务
1、使用事务进行操作

    [_queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        for (int i = 0; i<500; i++) {
            NSNumber *num = @(i+1);
            NSString *name = [[NSString alloc] initWithFormat:@"student_%d",i];
            NSString *sex = (i%2==0)?@"f":@"m";
            NSString *sql = @"insert into MYTAble(num,name,sex) values(?,?,?)";
            BOOL result = [db executeUpdate:sql,num,name,sex];
            if ( !result ) {
                NSLog(@" 多线程事务  插入失败!");
                //当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
                *rollback = YES;
                return;
            }else{
                NSLog(@" 多线程事务  插入成功!");
                NSLog(@"---%@",[NSThread currentThread]);
            }
        }
    }];



使用数据库操作的源码解析
其实就是创建一个串行队列,把响应操作放入到队列中,如果执行事务语句,如果执行失败,就会回退到事务开始的时候。


    FMDBRetain(self);
    dispatch_sync(_queue, ^() { 
        BOOL shouldRollback = NO;
        if (useDeferred) {
            [[self database] beginDeferredTransaction];
        }
        else {
            [[self database] beginTransaction];
        }
        block([self database], &shouldRollback);
        if (shouldRollback) {
            [[self database] rollback];
        }
        else {
            [[self database] commit];
        }
    });
    FMDBRelease(self);
- (BOOL)beginDeferredTransaction {
    BOOL b = [self executeUpdate:@"begin deferred transaction"];
    if (b) {
        _isInTransaction = YES;
    }
    return b;
}
- (BOOL)beginTransaction { 
    BOOL b = [self executeUpdate:@"begin exclusive transaction"];
    if (b) {
        _isInTransaction = YES;
    }
    return b;
}





六、遇到的问题
1、FMDB的使用的问题
a、不能在多个线程中使用同一个FMDatabase对象,FMDatabase本身不是线程安全的,这样使用会造成数据混乱、丢失等问题。
比如:我们开启2个线程写数据,一个线程对数据,这个时候你在一个线程中使用第一个FMDatabase来写nmber等于2学生的name字段改为tom,这时其他类创建第二个FMDatabase,并且开启另一个线程来写nmber等于2学生的name字段改为Marry,这个时候我们有开启了另外一个线程来读取nmber等于2学生的name字段,这时候你读取的是第一次写入的数据;也可能导致,该学生的最终名称还是第一次录入错误的tom,Marry先被写入,错误的名称tom覆盖了正确的marry。

b、FMDatabaseQueue中可能导致死锁。
比如:在FMDatabaseQueue中先执行a语句,可是a语句依赖下面b语句执行完成,而b语句又得依赖a语句的执行完成。这样就导致的死锁。

七、参考文件
sqlite入门基础-----http://blog.csdn.net/farsight2009/article/details/54847614
FMDB官方使用文档 ------http://www.360doc.com/content/14/1019/10/19663521_418095027.shtml
FMDB官方使用文档-GCD的使用-提高性能 ----http://www.cocoachina.com/industry/20130819/6821.html
iOS SQLite 使用FMDBMigrationManager 进行数据库迁移----https://www.aliyun.com/jiaocheng/372342.html

八、结束语
FMDB将SQLite API 进行了封装,在使用上非常方便。但是项目中到底使用哪种数据操作,还是要根据当时的业务需求出发。对于那些使用纯Sqlite API来进行数据库操作的app,我们可以考虑使用FMDB,这以提高对于以后数据库相关功能的开发维护的效率。
后期我们还会有coredata等数据存储方式的分析,希望大家后续继续关注。
如果有侵权的或者写的不好的地方,请大家指出,我会及时修改。

猜你喜欢

转载自blog.csdn.net/u014644610/article/details/80240618