FMDB v2.7
这是围绕 SQLite 的 Objective-C 封装:https://sqlite.ac.cn/
FMDB 邮件列表
http://groups.google.com/group/fmdb
阅读 SQLite FAQ
http://www.sqlite.org/faq.html
由于 FMDB 是基于 SQLite 构建的,您应该至少从头到尾阅读此页面一次。同时,请确保将 SQLite 文档页面书签:http://www.sqlite.org/docs.html
贡献
您是否有应加入 FMDB 的优秀想法?您可能需要先联系 ccgus,确保他还没有因为某些原因排除它。否则,请提交拉取请求,并确保您遵守本地编码约定。然而,请耐心等待,如果您一周或更长时间没有收到 ccgus 的回复,您可能需要发送一条消息询问发生了什么。
安装
CocoaPods
可以使用CocoaPods安装FMDB。
如果尚未进行,则可能需要初始化项目,以便为您生成Podfile模板。
$ pod init
然后,编辑Podfile,添加FMDB
。
# Uncomment the next line to define a global platform for your project
# platform :ios, '9.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 subspec。FMDB/SQLCipher subspec将SQLCipher声明为依赖项,这允许FMDB使用-DSQLITE_HAS_CODEC
标志进行编译。
Carthage
确保您有Carthage的最新版本后,您可以打开命令行终端,切换到您项目的主目录,然后执行以下命令。
$ echo ' github "ccgus/fmdb" ' > ./Cartfile
$ carthage update
然后,您可以按照Carthage的入门指南(即对于iOS,在目标中添加框架到“链接二进制库”并添加copy-frameworks
脚本;在macOS中,将框架添加到“嵌入式二进制文件”列表)配置您的项目。
FMDB类参考
http://ccgus.github.io/fmdb/html/index.html
自动引用计数(ARC)或手动内存管理?
您可以在您的Cocoa项目中使用任意一种样式。FMDB会在编译时确定您使用的是哪种样式,并做出正确的处理。
FMDB 2.7的新功能
FMDB 2.7试图支持更自然的界面。这对Swift开发者来说是一个相当重大的变化(经过nullability审计;将外部接口中的属性修改为方法,等等)。对于Objective-C开发者来说,这应该是一个相对无缝的过渡(除非您之前在公共接口中使用了已公开的ivars,但实际上您不应该这样做!)
可空性和Swift的可选对象
FMDB 2.7大体上与之前版本相同,但已经过可空性审计。对于Objective-C用户来说,这意味着如果您对基于FMDB的项目进行静态分析,在审查项目时可能会收到更有意义的警告,但可能不需要在代码中进行任何修改。
对于Swift用户来说,这次可空性审计导致了一些变化,这些变化与FMDB 2.6不完全向后兼容,但更加Swifty。在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池中,因此您不必这样做。
此外,在过去,在检索传递给函数的值时,您必须降至SQLite C API并包含您自己的sqlite3_value_XXX
调用。现在有了FMDatabase
方法,如valueInt
、valueString
等,因此您可以保持在使用Swift和/或Objective-C的同时,无需自己调用C函数。同样,当指定返回值时,您不再需要调用C API的sqlite3_result_XXX
,而是可以使用FMDatabase
方法,如resultInt
、resultString
等。有一个名为SqliteValueType
的新enum
,用于检查传递给自定义函数的参数类型。
因此,您可以在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变更。特别是,
-
为了与其他API保持一致,方法
objectForColumnName
和UTF8StringForColumnName
已被重命名为objectForColumn
和UTF8StringForColumn
。 -
注意,如果传递了无效的列名/索引,现在
objectForColumn
(以及相关的索引运算符)返回nil
。它以前返回NSNull
。 -
为了避免与执行事务的
FMDatabaseQueue
方法inTransaction
混淆,用来确定您是否在事务中,原来的inTransaction
方法已被替换为只读属性isInTransaction
。 -
一些函数已被转换为属性,包括
databasePath
、maxBusyRetryTimeInterval
、shouldCacheStatements
、sqliteHandle
、hasOpenResultSets
、lastInsertRowId
、changes
、goodConnection
、columnCount
、resultDictionary
、applicationID
、applicationIDString
、userVersion
、countOfCheckedInDatabases
、countOfCheckedOutDatabases
和countOfOpenDatabases
。对于Objective-C用户来说,影响不大,但对于Swift用户来说,则使得接口更加自然。注意:对于Objective-C开发者来说,以前版本的FMDB公开了许多实例变量(但我们希望您并未直接使用它们!),但不再公开这些实现细节。
URL方法
随着Apple从路径转向URL,现在有各种init
方法的NSURL
版本,以前只接受路径。
用法
FMDB 有三个主要类
FMDatabase
- 代表单个 SQLite 数据库。用于执行 SQL 语句。FMResultSet
- 代表在FMDatabase
上执行查询的结果。FMDatabaseQueue
- 如果您想要在多个线程中执行查询和更新,您将想要使用这个类。它将在下面的“线程安全性”部分中进行描述。
数据库创建
使用路径创建 FMDatabase
来指向 SQLite 数据库文件。此路径可以是以下三者之一:
- 文件系统路径。文件不必在磁盘上存在。如果不存在,它将为您创建。
- 空字符串(
@""
)。在临时位置创建空数据库。当FMDatabase
连接关闭时,此数据库将被删除。 NULL
。创建内存数据库。当FMDatabase
连接关闭时,此数据库将被销毁。
(有关临时和内存数据库的更多信息,请参阅有关该主题的 sqlite 文档:http://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];
}
FMResultSet
有许多方法以适当格式检索数据
intForColumn
longForColumn
longLongIntForColumn
boolForColumn
doubleForColumn
stringForColumn
dateForColumn
dataForColumn
dataNoCopyForColumn
UTF8StringForColumn
objectForColumn
这些方法也有 {type}ForColumnIndex:
的变体,可用于根据结果中列的位置检索数据,而不是根据列的名称。
通常,没有必要自己 -close
一个 FMResultSet
,因为当结果集被释放,或者父数据库关闭时,这将自动发生。
关闭
在完成数据库的查询和更新后,您应该调用 -close
来关闭 FMDatabase
连接,这样 SQLite 就会释放其在操作过程中获取的所有资源。
[db close];
事务
FMDatabase
可以通过调用相应的方法或执行 begin/end 事务语句来开始和提交事务。
多条语句和批处理相关内容
您可以使用 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]
。例如,在comment
可能是nil
(在本例中就是这样),您可以使用comment ?: [NSNull null]
语法,如果comment
不是nil
,将插入字符串;如果它是nil
,将插入[NSNull null]
。
在Swift中,您将使用executeUpdate(values:)
,这不仅是一个简洁的Swift语法,而且还能抛出错误进行适当的错误处理
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。
要做到这一点,你必须
- 将FMDB
src
文件夹中的相关.m
和.h
文件复制到你的项目。
你可以全部复制(这最简单),或者只复制你需要的文件。你可能至少需要FMDatabase
和 FMResultSet
。 FMDatabaseAdditions
提供了一些非常有用的方便方法,因此你也可能想要它。如果你的数据库访问是多线程的话,使用FMDatabaseQueue
会非常有用。但是,如果你选择不复制整个src
目录中的所有文件,那么你可能需要更新FMDB.h
以仅引用你项目中包含的文件。
注意,如果你将src
文件夹中的所有文件复制到你的项目中(这是一种推荐的做法),你可能想要将单个文件拖动到你的项目中,而不是拖动文件夹,因为如果你拖动文件夹,你不会收到添加桥接头(见下一点)的提示。
- 如果有提示创建“桥接头”,你应该这样做。如果没有提示,并且你没有桥接头,请添加一个。
关于桥接头的信息,请参阅同一项目中混用Swift和Objective-C。
-
在你的桥接头中,添加一行文本。
#import "FMDB.h"
-
使用
executeQuery
和executeUpdate
的变体,配合sql
和values
参数以及try
模式,如下所示。这些形式的executeQuery
和executeUpdate
都像Swift原生一样抛出错误。
如果进行以上操作,则可以编写使用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。
- FMDBMigrationManager,为FMDB提供SQLite模式迁移管理系统的工具:https://github.com/layerhq/FMDBMigrationManager
- FCModel,对喜欢直接SQL访问的人来说是Core Data的一个替代方案:https://github.com/marcoarment/FCModel
关于FMDB编码风格的快速提示
使用空格,而不是制表符。使用方括号,而不是点符号。观察FMDB如何使用花括号等,并坚持使用该样式。
报告错误
将你的错误缩小到尽可能少的代码量。你希望开发者能轻松地看到和重现你的错误。如果你有帮助,假装能够修复你错误的人正在同时发布3个大产品,参与多个开源项目,还有一个新出生的孩子,总的来说非常非常忙碌。
我们还在FMDB分发的main.m(FMDBReportABugFunction)中添加了一个模板函数来帮助你。
- 在Xcode中打开fmdb项目。
- 打开main.m并修改FMDBReportABugFunction以重现你的错误。
- 在代码中设置你的表。
- 执行查询或更新。
- 添加一些演示错误的断言。
然后你可以通过展示你整洁而紧凑的FMDBReportABugFunction,或者在FMDB邮件列表上发布,或者通过github FMDB错误报告器报告错误。
可选
找出错误所在,修复它,发送补丁或者在该邮件列表上提出。确保所有其他测试都是在你的修改之后进行的。
支持
FMDB的支持渠道是邮件列表(见上方)、在这里提交错误报告或者在Stack Overflow上。也就是说,支持是由社区在自愿的基础上提供的。
FMDB的开发由Flying Meat的Gus Mueller监督。如果FMDB对你有帮助,请考虑购买FM的应用或告诉所有的朋友。
授权
FMDB的授权包含在"License.txt"文件中。
如果你在酒吧里遇到了Gus Mueller或Rob Ryan,如果你觉得FMDB对你有帮助,你可以考虑买他们想喝的饮料。
当然,饮料是为他们准备的,如果你试图占便宜可就丢脸了。