FMDB v2.7

CocoaPods Compatible Carthage Compatible

这是 SQLite 的 Objective-C 封装。

FMDB 邮件列表

https://groups.google.com/group/fmdb

阅读 SQLite FAQ

https://www.sqlite.org/faq.html

由于 FMDB 构建于 SQLite 之上,您至少需要从头到尾阅读此页面一次。 同时,请务必将 SQLite 文档页面加入书签:https://www.sqlite.org/docs.html

贡献

您是否有一个很棒的想法,应该包含在 FMDB 中? 您可以考虑先 ping ccgus,以确保他没有因为某种原因而排除它。 否则,pull request 非常棒,并且请确保遵守本地编码约定。 但是,请耐心等待,如果您在一周或更长时间内没有收到 ccgus 的任何消息,您可能需要发送一条消息询问发生了什么。

安装

CocoaPods

可以使用 CocoaPods 安装 FMDB。

如果您尚未这样做,您可能需要初始化项目,以便它为您生成 Podfile 模板

$ pod init

然后,编辑 Podfile,添加 FMDB

# Uncomment the next line to define a global platform for your project
# platform :ios, '12.0'

target 'MyApp' do
    # Comment the next line if you're not using Swift and don't want to use dynamic frameworks
    use_frameworks!

    # Pods for MyApp2

    pod 'FMDB'
    # pod 'FMDB/FTS'   # FMDB with FTS
    # pod 'FMDB/standalone'   # FMDB with latest SQLite amalgamation source
    # pod 'FMDB/standalone/FTS'   # FMDB with latest SQLite amalgamation source and FTS
    # pod 'FMDB/SQLCipher'   # FMDB with SQLCipher
end

然后安装 pods

$ pod install

然后打开 .xcworkspace 而不是 .xcodeproj

有关 CocoaPods 的更多信息,请访问 https://cocoapods.org.cn

如果将 FMDB 与 SQLCipher 一起使用,则必须使用 FMDB/SQLCipher 子规格。 FMDB/SQLCipher 子规格声明 SQLCipher 作为依赖项,允许使用 -DSQLITE_HAS_CODEC 标志编译 FMDB。

Carthage

确保您拥有 最新版本的 Carthage 后,您可以打开一个命令行终端,导航到项目的主目录,然后执行以下命令

$ echo ' github "ccgus/fmdb" ' > ./Cartfile
$ carthage update

然后,您可以按照 Carthage 的 入门 中概述的方式配置您的项目(例如,对于 iOS,将框架添加到目标中的“Link Binary with Libraries”;并添加 copy-frameworks 脚本;在 macOS 中,将框架添加到“Embedded Binaries”列表中)。

Swift Package Manager

将 FMDB 声明为包依赖项。

.package(
    name: "FMDB", 
    url: "https://github.com/ccgus/fmdb", 
    .upToNextMinor(from: "2.7.12")),

在目标依赖项中使用 FMDB

.product(name: "FMDB", package: "FMDB")

FMDB 类参考

https://ccgus.github.io/fmdb/html/index.html

自动引用计数 (ARC) 或手动内存管理?

您可以在 Cocoa 项目中使用任一风格。 FMDB 将在编译时确定您正在使用的风格,并执行正确的操作。

FMDB 2.7 中的新功能

FMDB 2.7 尝试支持更自然的界面。 这代表了 Swift 开发人员的相当重要的变化(经过可空性审核;在外部接口中尽可能转换为属性而不是方法;等等)。 对于 Objective-C 开发人员来说,这应该是一个相当无缝的过渡(除非您使用了先前在公共接口中公开的 ivar,无论如何您都不应该这样做!)。

可空性和 Swift 可选项

FMDB 2.7 与之前的版本基本相同,但已经过可空性审核。 对于 Objective-C 用户,这仅仅意味着如果您对基于 FMDB 的项目执行静态分析,您可能会在查看项目时收到更有意义的警告,但您的代码可能几乎不需要任何更改。

对于 Swift 用户,此可空性审核会导致与 FMDB 2.6 不完全向后兼容的更改,但更像 Swift。 在 FMDB 经过可空性审核之前,Swift 不得不防御性地假设变量是可选项,但该库现在更准确地知道哪些属性和方法参数是可选项,哪些不是。

但是,这意味着为 FMDB 2.7 编写的 Swift 代码可能需要更改。 例如,考虑以下 FMDB 2.6 的 Swift 3/Swift 4 代码

queue.inTransaction { db, rollback in
    do {
        guard let db == db else {
            // handle error here
            return
        }

        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
    } catch {
        rollback?.pointee = true
    }
}

由于 FMDB 2.6 未经过可空性审核,Swift 推断 dbrollback 是可选项。 但是,现在,在 FMDB 2.7 中,Swift 现在知道,例如,上面的 dbrollback 都不能是 nil,因此它们不再是可选项。 因此,它变为

queue.inTransaction { db, rollback in
    do {
        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [1])
        try db.executeUpdate("INSERT INTO foo (bar) VALUES (?)", values: [2])
    } catch {
        rollback.pointee = true
    }
}

自定义函数

过去,在编写自定义函数时,通常必须包含自己的 @autoreleasepool 块,以避免在编写扫描大型表的函数时出现问题。 现在,FMDB 会自动将其包装在一个 autorelease pool 中,因此您不必这样做。

此外,过去,在检索传递给函数的值时,您必须下降到 SQLite C API 并包含自己的 sqlite3_value_XXX 调用。 现在有 FMDatabase 方法,valueIntvalueString 等,因此您可以留在 Swift 和/或 Objective-C 中,而无需自己调用 C 函数。 同样,在指定返回值时,您不再需要调用 sqlite3_result_XXX C API,而是可以使用 FMDatabase 方法,resultIntresultString 等。 有一个新的 enum 用于 valueType,称为 SqliteValueType,可用于检查传递给自定义函数的参数类型。

因此,您可以执行类似的操作(从 Swift 3 开始)

db.makeFunctionNamed("RemoveDiacritics", arguments: 1) { context, argc, argv in
    guard db.valueType(argv[0]) == .text || db.valueType(argv[0]) == .null else {
        db.resultError("Expected string parameter", context: context)
        return
    }

    if let string = db.valueString(argv[0])?.folding(options: .diacriticInsensitive, locale: nil) {
        db.resultString(string, context: context)
    } else {
        db.resultNull(context: context)
    }
}

然后您可以在 SQL 中使用该函数(在这种情况下,同时匹配 "Jose" 和 "José")

SELECT * FROM employees WHERE RemoveDiacritics(first_name) LIKE 'jose'

请注意,方法 makeFunctionNamed:maximumArguments:withBlock: 已重命名为 makeFunctionNamed:arguments:block:,以更准确地反映第二个参数的功能意图。

API 更改

除了上面提到的 makeFunctionNamed 之外,还有一些其他的 API 更改。 具体来说,

URL 方法

为了与 Apple 从路径到 URL 的转变保持一致,现在有各种 init 方法的 NSURL 演绎版,以前只接受路径。

用法

FMDB 中有三个主要类

  1. FMDatabase - 表示单个 SQLite 数据库。 用于执行 SQL 语句。
  2. FMResultSet - 表示在 FMDatabase 上执行查询的结果。
  3. FMDatabaseQueue - 如果您想在多个线程上执行查询和更新,您将需要使用这个类。 它在下面的“线程安全”部分中描述。

数据库创建

FMDatabase 是使用 SQLite 数据库文件的路径创建的。 此路径可以是以下三种之一

  1. 文件系统路径。 该文件不必存在于磁盘上。 如果它不存在,则为您创建它。
  2. 空字符串 (@"")。 在临时位置创建一个空数据库。 关闭 FMDatabase 连接后,将删除此数据库。
  3. NULL。 创建一个内存数据库。 关闭 FMDatabase 连接后,将销毁此数据库。

(有关临时和内存数据库的更多信息,请阅读有关该主题的 sqlite 文档:https://www.sqlite.org/inmemorydb.html

NSString *path = [NSTemporaryDirectory() stringByAppendingPathComponent:@"tmp.db"];
FMDatabase *db = [FMDatabase databaseWithPath:path];

打开

在您可以与数据库交互之前,必须将其打开。 如果资源或权限不足以打开和/或创建数据库,则打开失败。

if (![db open]) {
    // [db release];   // uncomment this line in manual referencing code; in ARC, this is not necessary/permitted
    db = nil;
    return;
}

执行更新

任何不是 SELECT 语句的 SQL 语句都符合更新条件。 这包括 CREATEUPDATEINSERTALTERCOMMITBEGINDETACHDELETEDROPENDEXPLAINVACUUMREPLACE 语句(以及更多)。 基本上,如果您的 SQL 语句不是以 SELECT 开头,则它是更新语句。

执行更新返回一个单值,一个 BOOL。 返回值 YES 表示更新已成功执行,返回值 NO 表示遇到了一些错误。 您可以调用 -lastErrorMessage-lastErrorCode 方法来检索更多信息。

执行查询

SELECT 语句是一个查询,并通过 -executeQuery... 方法之一执行。

如果成功,执行查询返回一个 FMResultSet 对象,如果失败,则返回 nil。 您应该使用 -lastErrorMessage-lastErrorCode 方法来确定查询失败的原因。

为了迭代查询的结果,您可以使用 while() 循环。 您还需要从一个记录“步进”到另一个记录。 使用 FMDB,最简单的方法是这样

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

在尝试访问查询中返回的值之前,您必须始终调用 -[FMResultSet next],即使您只期望一个

FMResultSet *s = [db executeQuery:@"SELECT COUNT(*) FROM myTable"];
if ([s next]) {
    int totalCount = [s intForColumnIndex:0];
}
[s close];  // Call the -close method on the FMResultSet if you cannot confirm whether the result set is exhausted.

FMResultSet 有许多方法可以以适当的格式检索数据

这些方法中的每一种也都有一个 {type}ForColumnIndex: 变体,用于根据结果中列的位置而不是列的名称来检索数据。

通常,您不需要自己 -close 一个 FMResultSet,因为当结果集耗尽时会自动关闭。但是,如果您只提取单个请求或任意数量的未耗尽结果集的请求,您需要对 FMResultSet 调用 -close 方法。

关闭连接

当您完成对数据库的查询和更新后,您应该 -close FMDatabase 连接,以便 SQLite 释放其在操作过程中获取的任何资源。

[db close];

事务

FMDatabase 可以通过调用适当的方法或执行 begin/end transaction 语句来开始和提交事务。

多个语句和批量操作

您可以使用 FMDatabase 的 executeStatements:withResultBlock: 在一个字符串中执行多个语句

NSString *sql = @"create table bulktest1 (id integer primary key autoincrement, x text);"
                 "create table bulktest2 (id integer primary key autoincrement, y text);"
                 "create table bulktest3 (id integer primary key autoincrement, z text);"
                 "insert into bulktest1 (x) values ('XXX');"
                 "insert into bulktest2 (y) values ('YYY');"
                 "insert into bulktest3 (z) values ('ZZZ');";

success = [db executeStatements:sql];

sql = @"select count(*) as count from bulktest1;"
       "select count(*) as count from bulktest2;"
       "select count(*) as count from bulktest3;";

success = [self.db executeStatements:sql withResultBlock:^int(NSDictionary *dictionary) {
    NSInteger count = [dictionary[@"count"] integerValue];
    XCTAssertEqual(count, 1, @"expected one record for dictionary %@", dictionary);
    return 0;
}];

数据清理

当向 FMDB 提供 SQL 语句时,您不应该尝试在插入之前“清理”任何值。相反,您应该使用标准的 SQLite 绑定语法。

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

? 字符被 SQLite 识别为要插入的值的占位符。执行方法都接受可变数量的参数(或这些参数的表示形式,例如 NSArrayNSDictionaryva_list),这些参数都为您进行了适当的转义。

以及,如何在 Objective-C 中使用带有 ? 占位符的 SQL

NSInteger identifier = 42;
NSString *name = @"Liam O'Flaherty (\"the famous Irish author\")";
NSDate *date = [NSDate date];
NSString *comment = nil;

BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", @(identifier), name, date, comment ?: [NSNull null]];
if (!success) {
    NSLog(@"error = %@", [db lastErrorMessage]);
}

注意:NSInteger 变量 identifier 这样的基本数据类型,应该作为 NSNumber 对象,使用 @ 语法实现,如上所示。或者您也可以使用 [NSNumber numberWithInt:identifier] 语法。

同样,SQL NULL 值应该作为 [NSNull null] 插入。例如,对于可能为 nilcomment (在本例中就是),您可以使用 comment ?: [NSNull null] 语法,如果 comment 不为 nil,则插入该字符串,如果为 nil,则插入 [NSNull null]

在 Swift 中,您将使用 executeUpdate(values:),它不仅是简洁的 Swift 语法,而且还会 throws 错误以便进行适当的错误处理。

do {
    let identifier = 42
    let name = "Liam O'Flaherty (\"the famous Irish author\")"
    let date = Date()
    let comment: String? = nil

    try db.executeUpdate("INSERT INTO authors (identifier, name, date, comment) VALUES (?, ?, ?, ?)", values: [identifier, name, date, comment ?? NSNull()])
} catch {
    print("error = \(error)")
}

注意: 在 Swift 中,您不必像在 Objective-C 中那样包装基本数字类型。但是,如果要插入一个可选字符串,您可能会使用 comment ?? NSNull() 语法(即,如果它是 nil,则使用 NSNull,否则使用该字符串)。

或者,您可以使用命名参数语法

INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)

参数必须以冒号开头。 SQLite 本身支持其他字符,但内部字典键会添加冒号前缀,因此不要在字典键中包含冒号。

NSDictionary *arguments = @{@"identifier": @(identifier), @"name": name, @"date": date, @"comment": comment ?: [NSNull null]};
BOOL success = [db executeUpdate:@"INSERT INTO authors (identifier, name, date, comment) VALUES (:identifier, :name, :date, :comment)" withParameterDictionary:arguments];
if (!success) {
    NSLog(@"error = %@", [db lastErrorMessage]);
}

关键是,不应使用 NSString 方法 stringWithFormat 手动将值插入到 SQL 语句本身中。也不应使用 Swift 字符串插值将值插入到 SQL 中。使用 ? 占位符表示要插入到数据库中的值(或在 SELECT 语句的 WHERE 子句中使用)。

使用 FMDatabaseQueue 和线程安全。

从多个线程同时使用单个 FMDatabase 实例是个坏主意。为每个线程创建一个 FMDatabase 对象一直是可以的。只是不要在线程之间共享单个实例,更不要在多个线程同时共享。最终会发生不好的事情,最终会导致崩溃,或者可能会出现异常,或者可能会有陨石从天而降并击中你的 Mac Pro。*这太糟糕了*。

所以不要实例化一个单一的 FMDatabase 对象并在多个线程中使用它。

相反,使用 FMDatabaseQueue。实例化一个单一的 FMDatabaseQueue 并在多个线程中使用它。FMDatabaseQueue 对象将同步和协调多个线程之间的访问。以下是如何使用它

首先,创建你的队列。

FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];

然后像这样使用它

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

一种在事务中轻松封装的方法可以这样做

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

Swift 等价物是

queue.inTransaction { db, rollback in
    do {
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [1])
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [2])
        try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [3])

        if whoopsSomethingWrongHappened {
            rollback.pointee = true
            return
        }

        // etc ...
    } catch {
        rollback.pointee = true
        print(error)
    }
}

(注意,从 Swift 3 开始,使用 pointee。但在 Swift 2.3 中,使用 memory 而不是 pointee。)

FMDatabaseQueue 将在序列化队列上运行这些块(因此得名)。因此,如果您同时从多个线程调用 FMDatabaseQueue 的方法,它们将按接收顺序执行。这样,查询和更新就不会互相干扰,每个人都很高兴。

注意:FMDatabaseQueue 方法的调用是阻塞的。因此,即使您传递了块,它们也不会在另一个线程上运行。

基于块创建自定义 sqlite 函数。

你可以做到这一点!有关示例,请在 main.m 中查找 -makeFunctionNamed:

Swift

您也可以在 Swift 项目中使用 FMDB。

为此,您必须

  1. 将 FMDB src 文件夹中的相关 .m.h 文件复制到您的项目中。

您可以复制所有文件(这是最简单的),或者只复制您需要的。您至少可能需要 FMDatabaseFMResultSetFMDatabaseAdditions 提供了一些非常有用的便捷方法,因此您可能也需要它。如果您要对数据库进行多线程访问,FMDatabaseQueue 也非常有用。但是,如果您选择不复制 src 目录中的所有文件,您可能需要更新 FMDB.h 以仅引用您项目中包含的文件。

请注意,如果您将所有文件从 src 文件夹复制到您的项目中(建议这样做),您可能需要将各个文件拖到您的项目中,而不是文件夹本身,因为如果您拖动文件夹,系统不会提示您添加桥接头文件(请参阅下一点)。

  1. 如果系统提示您创建“桥接头文件”,您应该这样做。如果未提示且您还没有桥接头文件,请添加一个。

有关桥接头文件的更多信息,请参阅 Swift and Objective-C in the Same Project

  1. 在您的桥接头文件中,添加一行,内容为

    #import "FMDB.h"
  2. 使用带有 sqlvalues 参数的 executeQueryexecuteUpdate 的变体,以及 try 模式,如下所示。executeQueryexecuteUpdate 的这些版本都以真正的 Swift 方式 throw 错误。

如果您执行上述操作,则可以编写使用 FMDatabase 的 Swift 代码。例如,从 Swift 3 开始

let fileURL = try! FileManager.default
    .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
    .appendingPathComponent("test.sqlite")

let database = FMDatabase(url: fileURL)

guard database.open() else {
    print("Unable to open database")
    return
}

do {
    try database.executeUpdate("create table test(x text, y text, z text)", values: nil)
    try database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", values: ["a", "b", "c"])
    try database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", values: ["e", "f", "g"])

    let rs = try database.executeQuery("select x, y, z from test", values: nil)
    while rs.next() {
        if let x = rs.string(forColumn: "x"), let y = rs.string(forColumn: "y"), let z = rs.string(forColumn: "z") {
            print("x = \(x); y = \(y); z = \(z)")
        }
    }
} catch {
    print("failed: \(error.localizedDescription)")
}

database.close()

历史

历史记录和更改可在其 GitHub 页面 上找到,并在 "CHANGES_AND_TODO_LIST.txt" 文件中进行了总结。

贡献者

FMDB 的贡献者包含在 "Contributors.txt" 文件中。

使用 FMDB 的其他项目,可能对有识别能力的开发者感兴趣。

关于 FMDB 编码风格的快速说明

空格,而不是制表符。方括号,而不是点表示法。看看 FMDB 已经用花括号等做了什么,并坚持这种风格。

报告错误

将您的错误减少到尽可能少的代码量。您希望使开发人员能够非常容易地看到和重现您的错误。如果这有帮助,假装可以修复您的错误的人正在积极发布 3 个主要产品,从事少量开源项目,有一个新生婴儿,并且通常非常非常忙。

我们甚至在 FMDB 发行版的 main.m (FMDBReportABugFunction) 中添加了一个模板函数来帮助您

然后,您可以通过显示您精简的 FMDBReportABugFunction 将其提交到 FMDB 邮件列表,或者您可以通过 github FMDB bug 报告器报告该错误。

可选

找出错误的位置,修复它,然后发送补丁或将其提交到邮件列表。确保在修改后所有其他测试都运行正常。

支持

FMDB 的支持渠道是邮件列表(见上文),在此处提交错误,或者可能在 Stack Overflow 上。也就是说,支持由社区提供并以自愿为基础。

FMDB 的开发由 Flying Meat 的 Gus Mueller 负责监督。如果 FMDB 对您有帮助,请考虑从 FM 购买应用程序或告诉您的所有朋友。

许可

FMDB 的许可证包含在 "License.txt" 文件中。

如果您碰巧在酒吧里遇到 Gus Mueller 或 Rob Ryan,如果 FMDB 对您有用,您可能会考虑为他们购买他们选择的饮料。

(当然,饮料是给他们的,你试图保留它太可耻了。)