優秀的第三方庫,README 也是很優秀的,理解了 README,會對使用帶來很多便利。
ARC 和 MRC
項目中使用 ARC 還是 MRC,對使用 FMDB 都沒有任何影響,FMDB 會在編譯項目時自動匹配。
使用
在 FMDB 中有三個重要的類:
-
FMDatabase
:是一個提供 SQLite 數據庫的類,用于執行 SQL 語句。 -
FMResultSet
:用在FMDatabase
中執行查詢的結果的類。 -
FMDatabaseQueue
:在多線程下查詢和更新數據庫用到的類。
數據庫創建
FMDatabase
是通過一個 SQLite 數據庫文件路徑創建的,此路徑可以是以下三者之一:
- 一個文件的系統路徑。磁盤中可以不存在此文件,因為如果不存在會自動為你創建。
- 一個空的字符串
@""
。會在臨時位置創建一個空的數據庫,當FMDatabase
連接關閉時,該數據庫會被刪除。 -
NULL
。會在內存中創建一個數據庫,當FMDatabase
連接關閉時,該數據庫會被銷毀。
// 創建數據庫示例
FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"];
打開數據庫
數據庫必須是打開狀態,才能與之交互。如果沒有足夠的資源和權限來打開\創建數據庫,數據庫會打開失敗。
數據庫更新
SQL 語句中除過 SELECT
語句都可以稱之為更新操作。包括 CREATE
,UPDATE
,INSERT
,ALTER
,COMMIT
,BEGIN
,DETACH
,DROP
,END
,EXPLAIN
,VACUUM
,REPLACE
等。一般只要不是以 SELECT
開頭的 SQL 語句,都是更新語句。
執行更新語句后會返回一個 BOOL
值,返回 YES
表示執行更新語句成功,返回 NO
表示出現錯誤,可以通過調用 -lastErrorMessage
和 -lastErrorCode
方法獲取更多錯誤信息。
數據庫查詢
通過調用 -executeQuery...
方法之一執行 SELECT
語句進行數據庫查詢操作。
執行查詢操作后,如果成功會返回一個 FMResultSet
對象,反之會返回 nil
。通過 -lastErrorMessage
和 -lastErrorCode
方法可以確定為什么會查詢失敗。
為了遍歷查詢結果,需要 while()
循環,然后逐條記錄查看。在 FMDB 中,可以通過下面的簡單方式實現:
FMResultSet *s = [db executeQuery:@"SELECT * FROM myTable"];
while ([s next]) {
// 每條記錄的檢索值
}
即使只需要獲取一個數據,也還是必須在訪問查詢結果前調用 -[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:
變體,是基于列的位置來查詢數據。
通常情況下,一個 FMResultSet
沒有必要手動 -close
,因為結果集合 (result set) 被釋放或者源數據庫關閉會自動關閉。
關閉數據庫
當對數據庫進行查詢和更新操作完成后,需要調用 -close
關閉數據庫 FMDatabase
的連接。
// 示例
[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 中意為即將插入的值的占位符,FMDB 執行語句的方法都接受多個參數 (或者參數集合,比如 NSArray
,NSDictionary
,va_list
),它們都會正確轉義。
也可以使用命名參數語法:
// 示例
INSERT INTO myTable VALUES (:id, :name, :value)
這些參數必須以冒號開頭,SQLite 自身支持其他字符,但是命名時字典的鍵內部以冒號開頭,就不能在你的字典的鍵中包含冒號。
// 示例
NSDictionary *argsDict = [NSDictionary dictionaryWithObjectsAndKeys:@"My Name", @"name", nil];
[db executeUpdate:@"INSERT INTO myTable (name) VALUES (:name)" withParameterDictionary:argsDict];
因此,不應該寫類似下面這行一樣的錯誤代碼:
// 錯誤示例
[db executeUpdate:[NSString stringWithFormat:@"INSERT INTO myTable VALUES (%@)", @"this has \" lots of ' bizarre \" quotes '"]];
而應該這樣寫:
// 正確示例
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", @"this has \" lots of ' bizarre \" quotes '"];
所有傳遞給 -executeUpdate:
方法的參數都必須是對象。下面寫法執行不會起作用而且會引發崩潰:
// 錯誤示例
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", 42];
插入一個數的正確方法是把這個數字包裝成 NSNumber
對象:
// 正確示例
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:42]];
也可以使用 -execute*WithFormat:
這個方法將數字轉換成字符串:
// 轉換成字符串示例
[db executeUpdateWithFormat:@"INSERT INTO myTable VALUES (@d)", 42];
-execute*WithFormat:
這些方法后面都可以接格式字符串參數,以下 % 百分號格式符都是可以識別的:%@
, %c
, %s
, %d
, %D
, %i
, %u
, %U
, %hi
, %hu
, %qi
, %qu
, %f
, %g
, %ld
, %lu
, %lld
, %llu
。使用其他格式符可能會出現不可預知的問題。出于某種原因,可能需要在你的 SQL 語句中使用 %
字符,應該使用百分號轉義一下 %%
。
FMDatabaseQueue 隊列和線程安全
在多線程中同時使用 FMDatabase 單例是極其錯誤的想法,會導致每個線程創建一個 FMDatabase 對象。不要跨線程使用單例,也不要同時跨多線程,不然會奔潰或者異常。
FMDatabase 這個類是線程不安全的,在多線程中使用 FMDatabase 單例是極其錯誤的想法。不能在多線程的環境中對數據庫 FMDatabase 進行讀寫,會出現奔潰或者異常,因為你不能保證你讀數據的同時另外一條線程不在寫數據。
因此不要實例化一個 FMDatabase 單例來跨線程使用。
相反,使用 FMDatabaseQueue,FMDatabaseQueue 跨線程使用是同步的,下面是它的使用方法:
第一,創建隊列。
// 創建 FMdatabaseQueue 示例
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
然后這樣使用:
// 示例
[queue inDatabase:^(FMDatabase *db) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
FMResultSet *rs = [db executeQuery:@"select * from foo"];
while ([rs next]) {
...
}
}];
把操作放在事務中也很簡單,比如:
// 示例
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:1]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:2]];
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:3]];
if (whoopsSomethingWrongHappened) {
*rollback = YES;
return;
}
// ...
[db executeUpdate:@"INSERT INTO myTable VALUES (?)", [NSNumber numberWithInt:4]];
}];
FMDatabase 將塊代碼 block 運行在一個串行隊列上,即使在多線程同時調用 FMDatabaseQueue 的方法,它們仍然還是順序執行。這種查詢和更新方式不會影響其它,是線程安全的。
基于 block 自定義 SQLite 函數
這是可以的,例子可以在 main.m 中的 makeFunctionNamed:
方法查看。
Swift
在 Swift 項目中也可以使用 FMDB,需要做以下步驟:
- 將 FMDB 的
.m
和.h
全部文件拖進你的項目。 - 如果 Xcode 提示創建橋接文件,需要點擊創建。如果沒有提示,且項目中也沒有橋接文件,需要手動添加。點此查看橋接文件更多信息
- 在橋接文件中,添加這行代碼:
#import "FMDB.h"
- 可以從 "src/extra/Swift Extension" 文件夾中拷貝
FMDatabaseVariadic.swift
文件到項目中,就可以使用executeUpdate
和executeQuery
多參數了。
做完上述幾步,就可以使用 FMDatabase 寫 Swift 代碼了。
// 示例
let documentsFolder = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)[0] as String
let path = documentsFolder.stringByAppendingPathComponent("test.sqlite")
let database = FMDatabase(path: path)
if !database.open() {
println("Unable to open database")
return
}
if !database.executeUpdate("create table test(x text, y text, z text)", withArgumentsInArray: nil) {
println("create table failed: \(database.lastErrorMessage())")
}
if !database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", withArgumentsInArray: ["a", "b", "c"]) {
println("insert 1 table failed: \(database.lastErrorMessage())")
}
if !database.executeUpdate("insert into test (x, y, z) values (?, ?, ?)", withArgumentsInArray: ["e", "f", "g"]) {
println("insert 2 table failed: \(database.lastErrorMessage())")
}
if let rs = database.executeQuery("select x, y, z from test", withArgumentsInArray: nil) {
while rs.next() {
let x = rs.stringForColumn("x")
let y = rs.stringForColumn("y")
let z = rs.stringForColumn("z")
println("x = \(x); y = \(y); z = \(z)")
}
} else {
println("select failed: \(database.lastErrorMessage())")
}
database.close()
本文實際是對 FMDB 的 README 簡單翻譯,以方便使用 FMDB。