iOS数据库FMDB的基础知识

FMDB简介

SQLite是一个轻量级的关系型数据库,在使用的时候需要加入libsqlite3.tbd 的依赖并引入 sqlite3.h 头文件即可. 但是,原生的SQLite API在使用上十分繁杂. 因此,FMDB是以OC的方式封装了SQLite的C语言API,使用起来比原生的 API 更加简便。

优点
  • 更加面向对象,省去了很多麻烦、冗余的C语言代码
  • 比苹果自带的Core Data框架,更加轻量级和灵活
  • 提供了多线程安全的数据库操作方法,有效地防止数据混乱
缺点
  • 因为它是OC语言封装的,只能在ios使用,跨平台操作存在局限性
使用步骤

导入libsqlite3.0框架,导入头文件FMDatabase.h

FMDB导入.png

推荐使用App Store里的Datum Free工具操作本地的.sqlite数据库文件

FMDB有三个主要的类

  1. FMDatabase
    一个FMDatabase对象就代表一个单独的SQLite数据库,用来执行SQL语句
  2. FMResultSet
    使用FMDatabase执行查询后的结果集
  3. FMDatabaseQueue
    用于在多线程中执行多个查询或更新,它是线程安全的


    FMDB.png

打开数据库

通过指定SQLite数据库文件路径来创建FMDatabase对象

FMDatabase *db = [FMDatabase databaseWithPath:path];
if (![db open]) {//在和数据库交互 之前,数据库必须是打开的
    NSLog(@"数据库打开失败!");
}
  • 文件路径有三种情况
    1. 具体文件路径
      如果不存在会自动创建

    2. 空字符串@""
      会在临时目录创建一个空的数据库
      当FMDatabase连接关闭时,数据库文件也被删除

    3. nil
      会创建一个内存中临时数据库,当FMDatabase连接关闭时,数据库会被销毁

执行更新

在FMDB中,除查询以外的所有操作,都称为“更新”,create、drop、insert、update、delete等;该方法返回BOOL型, 成功返回 YES, 失败返回 NO

  • 格式
    使用executeUpdate:方法执行更新
- (BOOL)executeUpdate:(NSString*)sql, ...
- (BOOL)executeUpdateWithFormat:(NSString*)format, ...
- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments

注意

- executeUpdate: 标准的 SQL 语句,参数用?来占位,参数必须是对象类型,不能是int,double,bool等基本数据类型
-executeUpdateWithFormat:使用字符串的格式化构建 SQL 语句,参数用%@、%d等来占位
-executeUpdate:withArgumentsInArray:也可以把对应的参数装到数组里面传进去,SQL语句中的参数用?代替

  • 示例
//增
[db executeUpdate:@“INSERT INTO t_student (name, age) VALUES (?,?);”,name,@(age)];
[db executeUpdate:@“INSERT INTO     
t_student(name,age) VALUES  (?,?);”withArgumentsInArray:@[name,@(age)]];

//删除
int idNum = 101;
[self.db executeUpdate:@“delete from t_student where id = ?;”,@(idNum)];
[self.db executeUpdateWithFormat:@“delete from t_student where name = %@;”,@“apple_name”];

//改
[db executeUpdate:@"UPDATE t_student SET age = ? WHERE name = ?;", @20, @"Jack"];

注意事项
可以插入一个空值,但是这里的空值不是nil,而是NSNull

[db executeUpdate:@"INSERT INTO t_student (name, age, sex) VALUES (?,?,?)",name,@(age),[NSNull null]];

同样在对FMDB数据库对象的属性值判断是否为空时,不能用 nil 来判断,而应该用null来判断

[[resultSet objectForColumnName:@"name"] isKindOfClass:[NSNull Class]];

执行查询

执行查询时,如果成功返回FMResultSet对象,错误返回nil;也可以使用-lastErrorCode和-lastErrorMessage获知错误信息

  • 格式
- (FMResultSet *)executeQuery:(NSString*)sql, ...
- (FMResultSet *)executeQueryWithFormat:(NSString*)format, ...
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments
  • 示例
// 查询数据
FMResultSet *rs = [db executeQuery:@"SELECT * FROM t_student"];

// 遍历结果集
while ([rs next]) {
    NSString *name = [rs stringForColumn:@"name"];
    int age = [rs intForColumn:@"age"];
    double score = [rs doubleForColumn:@"score"];
}

FMResultSet 提供了很多获取不同类型数据的方法

// 获取下一个记录
- (BOOL)next;
// 获取记录有多少列
- (int)columnCount;
// 通过列名得到列序号,通过列序号得到列名
- (int)columnIndexForName:(NSString *)columnName;
- (NSString *)columnNameForIndex:(int)columnIdx;
// 获取存储的整形值
- (int)intForColumn:(NSString *)columnName;
- (int)intForColumnIndex:(int)columnIdx;
// 获取存储的长整形值
- (long)longForColumn:(NSString *)columnName;
- (long)longForColumnIndex:(int)columnIdx;
// 获取存储的布尔值
- (BOOL)boolForColumn:(NSString *)columnName;
- (BOOL)boolForColumnIndex:(int)columnIdx;
// 获取存储的浮点值
- (double)doubleForColumn:(NSString *)columnName;
- (double)doubleForColumnIndex:(int)columnIdx;
// 获取存储的字符串
- (NSString *)stringForColumn:(NSString *)columnName;
- (NSString *)stringForColumnIndex:(int)columnIdx;
// 获取存储的日期数据
- (NSDate *)dateForColumn:(NSString *)columnName;
- (NSDate *)dateForColumnIndex:(int)columnIdx;
// 获取存储的二进制数据
- (NSData *)dataForColumn:(NSString *)columnName;
- (NSData *)dataForColumnIndex:(int)columnIdx;
// 获取存储的UTF8格式的C语言字符串
- (const unsigned cahr *)UTF8StringForColumnName:(NSString *)columnName;
- (const unsigned cahr *)UTF8StringForColumnIndex:(int)columnIdx;
// 获取存储的对象,只能是NSNumber、NSString、NSData、NSNull
- (id)objectForColumnName:(NSString *)columnName;
- (id)objectForColumnIndex:(int)columnIdx

FMDatabaseQueue

FMDatabase这个类是线程不安全的,如果在多个线程中同时使用一个FMDatabase实例,会造成数据混乱、程序崩溃、报告异常等问题。为了保证线程安全,FMDB提供方便快捷的FMDatabaseQueue类

FMDatabaseQueue 中创建了一个GCD的串行队列,同一个FMDatabaseQueue 共享同一个FMDatabase对象,每一个SQL操作都是同步执行,因此在多线程环境下保障了数据的安全有序的访问

FMDatabaseQueue.png

参考1
参考2这个比较有深度
首先使用一个数据库文件地址来初始化 FMDatabaseQueue ,然后就可以将一个闭包(block)传入inDatabase方法中.在闭包中操作数据库,而不是直接参与 FMDatabase 的管理

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];

---------------------------------FMDatabaseQueue.m内部实现----------------------------
- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {

        _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
        dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
}

dispatch_queue_create第二个参数 指定 DISPATCH_QUEUE_SERIAL 或者是NULL的时候,创建的队列是串行队列.所以,FMDatabaseQueue是一个串行队列。然后使用dispatch_queue_set_specific向_queue中设置一个kDispatchQueueSpecificKey标识

使用
[queue inDatabase:^(FMDatabase *db) {
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jack"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Rose"];
    [db executeUpdate:@"INSERT INTO t_student(name) VALUES (?)", @"Jim"];
    
    FMResultSet *rs = [db executeQuery:@"select * from t_student"];
    while ([rs next]) {
        // …
    }
}];

注意事项

-(void)inDatabase:(void (^)(FMDatabase *db))block不可以嵌套使用。原理很简单。基于_queue为同步串行队列,如果嵌套使用则会引起死锁

事务

参考1 参考2 详细-参考3
sqlite 本身是支持事务操作的,FMDB 作为对 sqlite 的封装自然也是支持的。
那什么是事务操作?
简单讲一次 executeUpdate (插入数据,本质是 sqlite3_exec)就是一次事务操作,其具体过程是:
开始新事务-->插入数据-->提交事务
我们向数据库中插入多少条数据,这个过程就会执行多少次,显然当插入的数据很多的时候这是非常耗时的。
事务操作的原理就是:所有任务执行完成后再将结果一次性提交到数据库

优点:
  • 使用事务操作要比不使用节省大量时间,尤其当数据非常多的时候,避免重复的步骤浪费时间
开启事务的作用
  • 使用事务处理就是将所有任务执行完成以后将结果一次性提交到数据库,如果此过程出现异常则会执行回滚操作,这样节省了大量的重复提交环节所浪费的时间。要么全部成功,要么全部失败,防止出现中间状态,以保证数据处理过程中始终处于正确的状态
FMDatabase使用事务的方法:
-(void)insertUserInfoWithDataArr:(NSArray *)dataArr{
    if (dataArr.count<1) {
       return;
    }
    FMDatabase *fmdb = self.fmdb;
    [fmdb open];//开启数据库
    [fmdb beginTransaction];//开启事务
    BOOL isRollBack = NO;
    @try {
      for (int i = 0; i<dataArr.count; i++) {
        MsgCenterModel *model = dataArr[i];
        NSArray *sqlDataArr = @[@(model.userId),model.name,model.age];
        BOOL isSuccess = [fmdb executeUpdate:@"INSERT INTO UserInfoTable (userId,name,age) values (?,?,?)" withArgumentsInArray:sqlDataArr];
        if (!isSuccess) {
           NSLog(@"update Failure");
        }
      }
    }
    @catch (NSException *exception) {
        isRollBack = YES;
        [fmdb rollback];//事务回滚
    }
    @finally {
        if (!isRollBack) {
        [fmdb commit];//提交事务
      }
    }
    [fmdb close];//关闭数据库
}

FMDatabaseQueue使用事务的方法:

//多线程事务 
- (void)transactionByQueue { 
  //开启事务
  [self.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) { 
       //当最后*rollback的值为YES的时候,事务回退,如果最后*rollback为NO,事务提交
       *rollback = YES;
       return; 
      } 
    }
  }]; 
}

源码分析

databaseWithPath
_db赋值为nil,也就是说真正构建_db不是在initWithPath:这个函数中,其实作者是将构建部分代码放到了open函数中

+ (instancetype)databaseWithPath:(NSString *)aPath {
    //FMDBReturnAutoReleased是为了让FMDB兼容MRC和ARC
    return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]);
}

- (instancetype)initWithPath:(NSString *)path {
    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;
        _logsErrors                 = YES;
        _crashOnErrors              = NO;
        _maxBusyRetryTimeInterval   = 2;
        _isOpen                     = NO;
    }
    return self;
}

open
open函数才是真正获取到数据库,其本质上也就是调用SQLite的C/C++接口 – sqlite3_open()

- (BOOL)open {
    if (_isOpen) {
        return YES;
    }
    
    // if we previously tried to open and it failed, make sure to close it before we try again
    if (_db) {
        [self close];
    }
    
    // now open database
    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];
    }
    
    _isOpen = YES;
    
    return YES;
}

executeUpdate的内部实现

- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments {
    return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orDictionary:nil orVAList:nil];
}

- (BOOL)executeUpdate:(NSString*)sql values:(NSArray *)values error:(NSError * __autoreleasing *)error {
    return [self executeUpdate:sql error:error withArgumentsInArray:values orDictionary:nil orVAList:nil];
}

- (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments {
    return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:arguments orVAList:nil];
}

- (BOOL)executeUpdate:(NSString*)sql withVAList:(va_list)args {
    return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args];
}

- (BOOL)executeUpdate:(NSString*)sql
                error:(NSError**)outErr
 withArgumentsInArray:(NSArray*)arrayArgs
         orDictionary:(NSDictionary *)dictionaryArgs
             orVAList:(va_list)args {

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 {
                NSString *message = [NSString stringWithFormat:@"Could not find index for %@", dictionaryKey];
                
                if (_logsErrors) {
                    NSLog(@"%@", message);
                }
                if (outErr) {
                    *outErr = [self errorWithMessage:message];
                }
            }
        }
    }

}

- (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt {
    
    if ((!obj) || ((NSNull *)obj == [NSNull null])) {
        sqlite3_bind_null(pStmt, idx);
    }
    
    // FIXME - someday check the return codes on these binds.
    else if ([obj isKindOfClass:[NSData class]]) {
        const void *bytes = [obj bytes];
        if (!bytes) {
            // it's an empty NSData object, aka [NSData data].
            // Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob.
            bytes = "";
        }
        sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC);
    }
    else if ([obj isKindOfClass:[NSDate class]]) {
        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]]) {
        
        if (strcmp([obj objCType], @encode(char)) == 0) {
            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) {
            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) {
            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) {
            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) {
            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) {
            sqlite3_bind_double(pStmt, idx, [obj floatValue]);
        }
        else if (strcmp([obj objCType], @encode(double)) == 0) {
            sqlite3_bind_double(pStmt, idx, [obj doubleValue]);
        }
        else if (strcmp([obj objCType], @encode(BOOL)) == 0) {
            sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0));
        }
        else {
            sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
        }
    }
    else {
        sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC);
    }
}


参考看一下
iOS FMDB多线程之FMDatabaseQueue
FMDB无法更新二进制的问题
FMDB详解
数据库操作工具SQLiteManager

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

推荐阅读更多精彩内容