在 iOS 开发中使用 FMDB 进行数据库操作

前言

FMDB 是对 SQLite 的 Objective-C 语言的轻量级封装。
本文结合 Github 页面的使用说明与项目中的使用经验,列出一些简单的使用方法。

安装 FMDB

使用 CocoaPods 安装,在 podfile 中添加以下内容:

pod 'FMDB'

在命令行运行

pod install

再在项目中包含头文件 FMDB.h 即可。

使用 FMDB

建立数据库

代码示例:

NSString *path = [[NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"tmp.sqlite"];
FMDatabase *db = [FMDatabase databaseWithPath:path];

以上的 path 处如果不存在该数据库,FMDB 会新建该文件。如果 path@"",则在临时目录下新建数据库,并在关闭后删除。如果 pathNULL,则会建立一个内存中的数据库。

打开、关闭数据库

在使用之前,必须执行打开操作:

if (![db open]) {
    db = nil;
    return;
}

如果打开失败,可能由于权限不足或资源不足。完成数据库操作后,需要关闭数据库:

[db close];

执行更新操作

SELECT 之外的操作都可以看作是更新操作,例如 CREATE / UPDATE / INSERT / DELETE 等。

更新操作使用 - executeUpdate: 方法,会返回 BOOL 值代表更新是否成功,如果是 NO 则发生了某些错误。可以使用 - lastErrorMessage 获取更多信息。

代码示例:

NSString *sql = [NSString stringWithFormat:@"delete from myTable",];
BOOL success = [db executeUpdate:sql];
if (!success) {
    NSLog(@"%@",[db lastErrorMessage]);
}

执行查询操作

SELECT 操作,使用 - executeQuery: 方法,返回 FMResultSet 对象(如果失败返回 nil 。同样地,可以使用 - lastErrorMessage 获取更多信息)。之后需要使用 while 循环来遍历结果。

典型代码:

FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
while ([s next]) {
    //retrieve values for each record
}

注意:即使结果只有一个,也需要调用 -[FMResultSet next]

FMResultSet 有许多方便的方法得到不同格式的数据:

intForColumn:
longForColumn:
longLongIntForColumn:
boolForColumn:
doubleForColumn:
stringForColumn:
dateForColumn:
dataForColumn:
dataNoCopyForColumn:
UTF8StringForColumnName:
objectForColumnName:

数据库操作语句的参数

可以按照标准 SQLite 语法,用 ? 表示参数,例如:

INSERT INTO myTable VALUES (?, ?, ?, ?)

- executeUpdate: 方法接受可变长参数来传入具体数据,示例代码:

NSString *sql = [NSString stringWithFormat:@"insert into myTable (%@, %@) values (?, ?)", kColumnPhone, kColumnRegion];
[db executeUpdate:sql, phone, region];

注意:参数必须是 NSObject 的子类,一些基础类型如 NSInteger 等必须进行封装才能插入到数据库中,如 @(1)

使用 FMDatabaseQueue 确保线程安全

在多线程中使用同一个 FMDatabase 的实例会出各种问题,因此需要使用 FMDatabaseQueue 来保证线程安全。用文件路径实例化一个 FMDatabaseQueue 之后可以在多个线程中使用。

示例代码:

NSString *path = [[NSSearchPathForDirectoriesInDomains( NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"tmp.sqlite"];
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:path];

之后可以在 block 中操作数据库,而不需要处理数据库的打开、关闭等:

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

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

也可以将一系列语句放在一个事务(transaction)中处理:

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

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

以上是同步执行(dispatch_sync)的,因此将要使用数据库操作结果的语句放于其后即可,不需要使用回调。

实际上,FMDatabaseQueue 对象会建立一个专门的串行 dispatch_queue_t 来同步地执行 inDatabaseinTransaction block 中的代码,与在哪个线程中使用 FMDatabaseQueue 无关。这样会将分散的数据库操作集中起来,以避免并发操作可能带来的问题。在不同线程中执行的区别只是哪个线程等待数据库操作执行完毕而已。

多线程使用示例

示例代码:

@property (nonatomic, strong) FMDatabaseQueue *databaseQueue;

self.databaseQueue = [FMDatabaseQueue databaseQueueWithPath:path];

dispatch_queue_t myQ = dispatch_queue_create("My Queue", NULL);
dispatch_async(myQ, ^{
    [self.databaseQueue inTransaction:^(FMDatabase *db, BOOL *rollback) {
        for (MyObject *object in objectsToInsert) {
            // 数据库更新操作
        }
     }];

     // 如果数据库更新操作完成需要更新 UI,再回到主线程
     dispatch_async(dispatch_get_main_queue(), ^{
         // 更新 UI
     });
});

当需要进行的更新操作很多时(例如以上的循环操作),相对 inDatabase 而言,使用 inTransaction 可以大大提高效率。这是因为事务 inTransaction 只在完成后提交(commit)一次,而 inDatabase 会在每次更新完成后提交一次。