这是 SQLite 的 Objective-C 封装。
https://groups.google.com/group/fmdb
https://www.sqlite.org/faq.html
由于 FMDB 构建于 SQLite 之上,您至少需要从头到尾阅读此页面一次。 同时,请务必将 SQLite 文档页面加入书签:https://www.sqlite.org/docs.html
您是否有一个很棒的想法,应该包含在 FMDB 中? 您可以考虑先 ping ccgus,以确保他没有因为某种原因而排除它。 否则,pull request 非常棒,并且请确保遵守本地编码约定。 但是,请耐心等待,如果您在一周或更长时间内没有收到 ccgus 的任何消息,您可能需要发送一条消息询问发生了什么。
可以使用 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 后,您可以打开一个命令行终端,导航到项目的主目录,然后执行以下命令
$ echo ' github "ccgus/fmdb" ' > ./Cartfile
$ carthage update
然后,您可以按照 Carthage 的 入门 中概述的方式配置您的项目(例如,对于 iOS,将框架添加到目标中的“Link Binary with Libraries”;并添加 copy-frameworks
脚本;在 macOS 中,将框架添加到“Embedded Binaries”列表中)。
将 FMDB 声明为包依赖项。
.package(
name: "FMDB",
url: "https://github.com/ccgus/fmdb",
.upToNextMinor(from: "2.7.12")),
在目标依赖项中使用 FMDB
.product(name: "FMDB", package: "FMDB")
https://ccgus.github.io/fmdb/html/index.html
您可以在 Cocoa 项目中使用任一风格。 FMDB 将在编译时确定您正在使用的风格,并执行正确的操作。
FMDB 2.7 尝试支持更自然的界面。 这代表了 Swift 开发人员的相当重要的变化(经过可空性审核;在外部接口中尽可能转换为属性而不是方法;等等)。 对于 Objective-C 开发人员来说,这应该是一个相当无缝的过渡(除非您使用了先前在公共接口中公开的 ivar,无论如何您都不应该这样做!)。
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 推断 db
和 rollback
是可选项。 但是,现在,在 FMDB 2.7 中,Swift 现在知道,例如,上面的 db
和 rollback
都不能是 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
方法,valueInt
、valueString
等,因此您可以留在 Swift 和/或 Objective-C 中,而无需自己调用 C 函数。 同样,在指定返回值时,您不再需要调用 sqlite3_result_XXX
C API,而是可以使用 FMDatabase
方法,resultInt
、resultString
等。 有一个新的 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:
,以更准确地反映第二个参数的功能意图。
除了上面提到的 makeFunctionNamed
之外,还有一些其他的 API 更改。 具体来说,
为了与 API 的其余部分保持一致,方法 objectForColumnName
和 UTF8StringForColumnName
已重命名为 objectForColumn
和 UTF8StringForColumn
。
请注意,如果将无效的列名/索引传递给 objectForColumn
(以及关联的下标运算符),则现在返回 nil
。 它过去返回 NSNull
。
为了避免与执行事务的 FMDatabaseQueue
方法 inTransaction
混淆,用于确定您是否在事务中的 FMDatabase
方法 inTransaction
已替换为只读属性 isInTransaction
。
一些函数已转换为属性,即 databasePath
、maxBusyRetryTimeInterval
、shouldCacheStatements
、sqliteHandle
、hasOpenResultSets
、lastInsertRowId
、changes
、goodConnection
、columnCount
、resultDictionary
、applicationID
、applicationIDString
、userVersion
、countOfCheckedInDatabases
、countOfCheckedOutDatabases
和 countOfOpenDatabases
。 对于 Objective-C 用户来说,这几乎没有实际影响,但对于 Swift 用户来说,这会产生一个稍微自然的界面。 注意:对于 Objective-C 开发人员,以前版本的 FMDB 公开了许多 ivar(但我们希望您无论如何都没有直接使用它们!),但这些的实现细节不再公开。
为了与 Apple 从路径到 URL 的转变保持一致,现在有各种 init
方法的 NSURL
演绎版,以前只接受路径。
FMDB 中有三个主要类
FMDatabase
- 表示单个 SQLite 数据库。 用于执行 SQL 语句。FMResultSet
- 表示在 FMDatabase
上执行查询的结果。FMDatabaseQueue
- 如果您想在多个线程上执行查询和更新,您将需要使用这个类。 它在下面的“线程安全”部分中描述。FMDatabase
是使用 SQLite 数据库文件的路径创建的。 此路径可以是以下三种之一
@""
)。 在临时位置创建一个空数据库。 关闭 FMDatabase
连接后,将删除此数据库。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 语句都符合更新条件。 这包括 CREATE
、UPDATE
、INSERT
、ALTER
、COMMIT
、BEGIN
、DETACH
、DELETE
、DROP
、END
、EXPLAIN
、VACUUM
和 REPLACE
语句(以及更多)。 基本上,如果您的 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
有许多方法可以以适当的格式检索数据
intForColumn
longForColumn
longLongIntForColumn
boolForColumn
doubleForColumn
stringForColumn
dateForColumn
dataForColumn
dataNoCopyForColumn
UTF8StringForColumn
objectForColumn
这些方法中的每一种也都有一个 {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 识别为要插入的值的占位符。执行方法都接受可变数量的参数(或这些参数的表示形式,例如 NSArray
、NSDictionary
或 va_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]
插入。例如,对于可能为nil
的comment
(在本例中就是),您可以使用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
子句中使用)。
从多个线程同时使用单个 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
方法的调用是阻塞的。因此,即使您传递了块,它们也不会在另一个线程上运行。
你可以做到这一点!有关示例,请在 main.m 中查找 -makeFunctionNamed:
您也可以在 Swift 项目中使用 FMDB。
为此,您必须
src
文件夹中的相关 .m
和 .h
文件复制到您的项目中。您可以复制所有文件(这是最简单的),或者只复制您需要的。您至少可能需要 FMDatabase
和 FMResultSet
。FMDatabaseAdditions
提供了一些非常有用的便捷方法,因此您可能也需要它。如果您要对数据库进行多线程访问,FMDatabaseQueue
也非常有用。但是,如果您选择不复制 src
目录中的所有文件,您可能需要更新 FMDB.h
以仅引用您项目中包含的文件。
请注意,如果您将所有文件从 src
文件夹复制到您的项目中(建议这样做),您可能需要将各个文件拖到您的项目中,而不是文件夹本身,因为如果您拖动文件夹,系统不会提示您添加桥接头文件(请参阅下一点)。
有关桥接头文件的更多信息,请参阅 Swift and Objective-C in the Same Project。
在您的桥接头文件中,添加一行,内容为
#import "FMDB.h"
使用带有 sql
和 values
参数的 executeQuery
和 executeUpdate
的变体,以及 try
模式,如下所示。executeQuery
和 executeUpdate
的这些版本都以真正的 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 已经用花括号等做了什么,并坚持这种风格。
将您的错误减少到尽可能少的代码量。您希望使开发人员能够非常容易地看到和重现您的错误。如果这有帮助,假装可以修复您的错误的人正在积极发布 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 对您有用,您可能会考虑为他们购买他们选择的饮料。
(当然,饮料是给他们的,你试图保留它太可耻了。)