理論篇
本文默認(rèn)你已經(jīng)在項目中繼承了FMDB,如果是swift項目,已經(jīng)在swift中引入橋接文件并添加了FMDB。
iOS數(shù)據(jù)庫常用的庫有CoreData、FMDB、Realm等。對于三種方式的優(yōu)缺點及速度等本文不做討論,由于FMDB的受歡迎程度很高,本文對FMDB進(jìn)行探討和梳理。
首先三種數(shù)據(jù)庫操作方式的簡介:
- CoreData是Apple對sqlite的封裝,一種面向?qū)ο蟮臄?shù)據(jù)庫操作方式。包含
.xcdatamodeld的模型可視化操作,多表的關(guān)聯(lián),數(shù)據(jù)的變更檢測等,功能強大。了解更多 認(rèn)識CoreData-基礎(chǔ)使用 - FMDB是開源的對sqlite的封裝,使用sql語句進(jìn)行對數(shù)據(jù)庫的操作,靈活方便,本文主要討論這個。了解更多SQLite的常見問題
- Realm是由Y Combinator孵化的創(chuàng)業(yè)團隊開源出來的一款可以用于iOS(同樣適用于Swift&Objective-C)和Android的跨平臺移動數(shù)據(jù)庫。目前最新版是Realm 2.0.2,支持的平臺包括Java,Objective-C,Swift,React Native,Xamarin。提供可視化工具。Realm與sqlite無關(guān)。了解更多Realm數(shù)據(jù)庫 從入門到“放棄”
FMDB主要類
- FMDatabase-代表一個獨立的SQLite數(shù)據(jù)庫,執(zhí)行SQL語句。
- FMResultSet-代表FMDatebase查詢的結(jié)果集,
- FMDatabaseQueue-如果你想要在多線程中查詢和更新,你應(yīng)該使用這個類,
FMDB創(chuàng)建數(shù)據(jù)庫
FMDatabase通過一個SQLite數(shù)據(jù)庫文件的路徑創(chuàng)建。這個路徑可以有以下三種樣式:
- 一個系統(tǒng)文件路徑.硬盤上之前不存在的,如果它不存在,F(xiàn)MDB會為你新建。
- 一個空字符串,一個空數(shù)據(jù)庫在臨時文件中創(chuàng)建。在數(shù)據(jù)庫連接關(guān)閉的時候,這個數(shù)據(jù)庫會被刪除。
- NULL(空)。一個在內(nèi)存中的數(shù)據(jù)庫將被創(chuàng)建。在數(shù)據(jù)庫連接關(guān)閉的時候,這個數(shù)據(jù)庫會被銷毀。
eg:
//OC
FMDatabase *db = [FMDatabase databaseWithpath:@"/tmp/tmp.db"];
//想了解更多關(guān)于臨時或內(nèi)存數(shù)據(jù)庫,請閱讀sqlite文檔:http://www.sqlite.org/inmemorydb.html
//swift
let db = FMDatabase.init(path: "/tmp/tmp.db")
FMDB打開數(shù)據(jù)庫
和數(shù)據(jù)庫建立連接之前,應(yīng)該確保它是打開的,在內(nèi)存不足、禁止開啟、創(chuàng)建數(shù)據(jù)庫的時候會打開失敗。
//OC
if(![db open]){
//打開數(shù)據(jù)庫失敗
return;
}
//Swift
if (db?.open())! {
//打開成功
}else{
//打開失敗
}
FMDB執(zhí)行更新
除了SELECT格式的數(shù)據(jù)庫執(zhí)行語句都是更新。包括CREATE,UPDATE,INSERT,ALTER,COMMIT,BEGIN,DETACH,DELETE,DROP,END,EXPLAIN,VACUUM,還有replace語句等等?;旧?,只要你的SQL語句不是以SELECT開頭,都是更新語句。
執(zhí)行更新使用executeUpdate方法,返回一個單一BOOL值,返回值為YES代表執(zhí)行更新成功,返回值為NO表示發(fā)生了一些錯誤。你可以調(diào)用laseErrorMessage和laseErrorCode方法接收更多錯誤信息。
//OC
BOOL sucess = [db executeUpdate:@"DELETE FROM person WHERE person_id = ?",person.ID];
if sucess {
}else{
NSLog(@"執(zhí)行出錯了:%@",[db laseErrorMessage])
}
//swift
let sql = String.init(format: "DELETE FROM person WHERE person_id = %d'", person.ID)
guard let sucess = db?. executeUpdate(sql, withArgumentsIn: nil) else{
debugPrint("\(String(describing: db?.lastErrorMessage()))")
}
if sucess {
}else{
debugPrint("執(zhí)行出錯了")
}
FMDB執(zhí)行查詢
一個SELECt語句是一個查詢語句并且通過-executrQuery方法執(zhí)行。
執(zhí)行查詢成功返回FMResultSet對象,失敗返回nil。你應(yīng)該使用laseErrorMessage和laseErrorCode方法確定到底為什么查詢失敗。
為了循環(huán)訪問查詢結(jié)果集,你可以使用while()循環(huán).你也需要從一個紀(jì)錄到另一條。在FMDB中,最簡單的辦法是這樣:
//OC
FMResultSet *s = [db executeQuery:@"SELECT *FROM myTable"];
while ([s next]){
//retrieve values for each record
}
//swift
let sql = "SELECT *FROM myTable"
guard let result = db?.executeQuery(checksql, withArgumentsIn: nil) else {
debugPrint("\(String(describing: db?.lastErrorMessage()))")
return
}
while result.next() {
//retrieve values for each record
}
通常你在使用查詢結(jié)果的返回值前必須先調(diào)用FMResultSet的next方法,即使你只需要一次,像這樣:
//OC
FMResultSet *s = [db executeQuery:@“SELECT COUNT(*) FROM myTable”];
if ([s next]) {
int totalCount = [s intForColumnIndex:0];
}
FMResultSet 擁有許多方法去獲取適當(dāng)類型的數(shù)據(jù)
intForColumn:
longForColumn:
longLongIntForColumn:
boolForColumn:
doubleForColumn:
stringForColumn:
dateForColumn:
dataForColumn:
dataNoCopyForColumn:
UTF8StringForColumnName:
objectForColumnName:
其中時間date類型取出時使用dateForColumn方法。
這里的每一個方法都有一個對應(yīng)的{type}ForColumnIndex:表達(dá)式?;谧侄卧诮Y(jié)果集中的位置可以被用來獲取數(shù)據(jù),和字段名一一對應(yīng)。
特別的,你在這里不需要close一個FMResultSet,直到結(jié)果集都被釋放或者父數(shù)據(jù)庫關(guān)閉了。
FMDB關(guān)閉(Close)
當(dāng)你完成了數(shù)據(jù)的查詢和更新,你應(yīng)該close這個FMDatabase的連接讓SQLite釋放那些操作過程中占用的資源。
FMDB 事務(wù)(Transactions)
FMDatabase可以通過調(diào)用合適的方法或者執(zhí)行開始和結(jié)束事務(wù)型語句開始并提交一個事務(wù)。多條語句和批量添加你可以使用FMDatabase的executeStatements:withResultBlock:去做。
一個多條SQL語句在一個字符串中:
//OC
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;
}];
OC中對于長字符串的處理,對每一段字符串用雙引號"括起來,可以換行,直到以分號;結(jié)尾,表示字符串結(jié)束。
NSString *aa = @"長字符串的第一段"
"這是第二段"
"出現(xiàn)分號則表示字符串結(jié)束"
;
FMDB線程安全(FMDatabaseQueue)
在多線程使用同一個FMDatabase的單例是一個壞主意。通常為每一個線程創(chuàng)建FMDatabase對象都是OK的,但千萬不要跨線程使用數(shù)據(jù)庫單例。在多線程同時使用,你可能會遇到異?;蜷W退。
如果需要線程安全,用FMDatabaseQueue替代。
實例化一個FMDatabaseQueue的單例,并且在多線程中使用這個單例,將通過隊列管理,同步執(zhí)行來自多線程的命令。
這里是如何使用,第一步,創(chuàng)建你的隊列。
//OC
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:aPath];
//swift
let queue = FMDatabaseQueue.init(path: aPath)
然后這樣使用它:
//OC
[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]) {
…
}
}];
擴展:在swift中使用queue的單例的示例代碼:
//swift
class DBHelper: NSObject {
static let dbQueue:FMDatabaseQueue? = {
let dbPath = XYWSandBox.getDocumentDirectory() + "/114la.db"
let dbq = FMDatabaseQueue.init(path: dbPath)
return dbq
}()
//類方法,dbQueue是DBHelper單例(static)
class func dbQueryAlldownload() -> [DownloadData]{
let resultArray = [DownloadData]()
self.dbQueue?.inDatabase({ (db) in
let sql = "select * from \(self.downloadsTable) ORDER BY id Desc"
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
var resultArray = [DownloadData]()
while result.next() {
//獲取數(shù)據(jù)并創(chuàng)建download對象
resultArray.append(download)
}
})
return resultArray
}
}
將簡單的多任務(wù)包裝在一個事務(wù)中,使用queue的inTransaction方法,當(dāng)
//OC
[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];
}];
//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//早版本swift使用rollback.memory = true
return
}
try db.executeUpdate("INSERT INTO myTable VALUES (?)", values: [4])
} catch {
rollback.pointee = true//早版本swift使用rollback.memory = true
print(error)
}
}
FMDatabaseQueue將在隊列中順序執(zhí)行代碼塊中的任務(wù),因此你可以同時調(diào)用在多線程的多FMDatabaseQueue的方法,在順序排到的時候它們將被執(zhí)行。需要注意的是,在一個queue的block中,不能再次使用queue的block。
實戰(zhàn)篇
實戰(zhàn)場景
我們做一個app,這個app中可以下載網(wǎng)絡(luò)內(nèi)容,我們的下載管理界面中需要顯示一些信息,那么大概需要記錄下載任務(wù)的內(nèi)容有:下載地址,下載文件名,文件大小,已下載大小,創(chuàng)建下載任務(wù)的源網(wǎng)頁地址,創(chuàng)建時間。
我們自己也有個網(wǎng)站,是個說明書大全網(wǎng)站,因為說明書是由我們自己提供的內(nèi)容,所以要能通過已下載的文件,分享這個文件的H5在線閱讀的url地址,在第三方分享的時候要有封面圖的遠(yuǎn)程url地址,并且在已下載文件里這個說明書可以進(jìn)行“反饋”、“舉報”。那么我們就需要知道這個說明書的“id”等信息。因此我們創(chuàng)建一個表單獨記錄這個說明書的信息。
在此功能完成中,需要“下載管理類”、“數(shù)據(jù)庫管理類”、“沙盒文件管理類”相互配合。
其中通過文件的下載url作為關(guān)聯(lián)標(biāo)識符,以此url獲取下載的信息,數(shù)據(jù)的信息,以及文件路徑等信息。
下載管理類主要負(fù)責(zé)文件的下載,暫停,恢復(fù),斷點下載等;沙盒文件管理類主要負(fù)責(zé)下載文件的管理;數(shù)據(jù)庫管理類負(fù)責(zé)記錄下載的信息,以及其他需要持久化的信息。
因此關(guān)于下載后的文件路徑地址,并不在數(shù)據(jù)庫中存儲,而是使用了另外的文件管理類,通過url地址等來獲取相應(yīng)的下載后的路徑。此類不在此文探討范圍,暫且不表。
下載的步驟如下:
- 根據(jù)發(fā)起請求的header中的Content-Type檢測打開的是個可下載文件。
- 創(chuàng)建下載任務(wù),并在下載數(shù)據(jù)表中記錄這個下載。
- 檢查是否是檢查網(wǎng)址是否來自自家網(wǎng)站,如果是,在說明書表中記錄信息。
- 文件下載過程更新下載數(shù)據(jù)表中的信息。
顯示步驟如下:
- 從下載數(shù)據(jù)表中查詢所有內(nèi)容,或者根據(jù)需求查詢某些內(nèi)容。
- 已下載文件的封面為各種格式的默認(rèn)圖,顯示大小、時間等其他信息。
- 根據(jù)下載的url從說明書數(shù)據(jù)表中查詢信息。
- 如果2沒有找到說明書,不顯示反饋、分享等工具欄,只顯示文件的打開方式。
- 如果2有結(jié)果,則顯示此下載關(guān)于說明書的封面圖等其他信息。
實戰(zhàn)代碼
在項目應(yīng)用中,個人推薦所有關(guān)于數(shù)據(jù)庫的操作都放在一個類中。
通過swift的extension,將不同表的操作放在不同的區(qū)域,并且通過文檔注釋//MARK: -的方法,建立快速索引。
這樣就很容易管理數(shù)據(jù)庫,并且此類以外所有地方都不在與數(shù)據(jù)庫交互。
這里我們創(chuàng)建一個數(shù)據(jù)庫管理類DBHelper:
//
// DBManager.swift
// Browser
//
// Created by 西方 on 2018/1/24.
// Copyright ? 2018年 114la.com. All rights reserved.
//
import UIKit
/// 數(shù)據(jù)庫管理類,可在需要操作數(shù)據(jù)庫的時候,通過擴展的模式添加方法,統(tǒng)一管理便于維護
class DBHelper: NSObject {
/// 單例的dbQueue
static let dbQueue:FMDatabaseQueue? = {
let dbPath = XYWSandBox.getDocumentDirectory() + "/114la.db"
let dbq = FMDatabaseQueue.init(path: dbPath)
return dbq
}()
/// 下載信息的表
static let downloadsTable:String = {
return "downloads"
}()
/// 說明書信息的表
static let instructionsCacheTable:String = {
return "instructionsCache"
}()
}
//MARK: - 下載表的數(shù)據(jù)庫操作
extension DBHelper {
/// 創(chuàng)建下載表
class func createDownloadsTable() {
self.dbQueue?.inDatabase({ (db) in
let sql = "create table if not exists \(self.downloadsTable) (id integer primary key autoincrement,url text,weburl text,title text,downloadsize long,filesize long,createtime datetime)"
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
//表創(chuàng)建成功
debugPrint("createDownloadsTable sucess")
}else{
debugPrint("downloads table create faild!")
MBProgressHUD.showFailImage("downloads table create faild!")
}
})
}
typealias DownloadResultCompleteHandle = ([DownloadData]) -> ()
/// 查詢所有的下載數(shù)據(jù)
///
/// - Parameter complete: 完成回調(diào)
class func quryAllDownloads(complete:@escaping DownloadResultCompleteHandle){
self.dbQueue?.inDatabase({ (db) in
let sql = "select * from \(self.downloadsTable) ORDER BY id Desc"
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
var resultArray = [DownloadData]()
while result.next() {
let url = result.string(forColumn: "url") ?? ""
let webUrl = result.string(forColumn: "webUrl") ?? ""
let title = result.string(forColumn: "title") ?? "下載出錯"
let downloadsize = result.double(forColumn: "downloadsize")
let filesize = result.double(forColumn: "filesize")
let createtime = result.date(forColumn: "createtime")
let download = DownloadData.init()
download.url = url
download.sourceUrl = webUrl
download.title = title
download.downloadsize = downloadsize
download.filesize = filesize
download.createTime = createtime
resultArray.append(download)
}
complete(resultArray)
return
})
}
/// 查詢某個url是否已經(jīng)下載過
///
/// - Parameter url: URL地址
/// - Returns: 是否下載過
class func isDownloadExit(_ url:String)->Bool{
var exit = false
self.dbQueue?.inDatabase({ (db) in
let checksql = String.init(format: "select * from %@ where url = '%@'",self.downloadsTable, url.urlEncoded())
guard let exitresult = db?.executeQuery(checksql, withArgumentsIn: nil) else {
debugPrint("\(String(describing: db?.lastErrorMessage()))")
return
}
if exitresult.next() {
debugPrint("下載已存在!")
exit = true
}
})
return exit
}
/// 添加一個下載任務(wù)
///
/// - Parameters:
/// - url: 下載地址
/// - title: 任務(wù)的標(biāo)題
class func dbAddDownload(_ url:String,webUrl:String,title:String){
self.dbQueue?.inDatabase({ (db) in
debugPrint("準(zhǔn)備添加新的下載數(shù)據(jù)!")
let now = Date.init().timeIntervalSince1970
let sql = String.init(format: "insert into %@ (url,weburl,title,downloadsize,filesize,createtime) values ('%@','%@','%@',0,1,%f)" ,self.downloadsTable,url.urlEncoded(),webUrl.urlEncoded(),title.urlEncoded(),now)
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
debugPrint("dbAddDownload - \(url)")
}else{
MBProgressHUD.showFailImage("db add download falid!")
debugPrint("db add download falid! - \(url) \n \(String(describing: db?.lastErrorMessage()))")
}
})
}
/// 更新下載的進(jìn)度信息
///
/// - Parameter data: 下載數(shù)據(jù)data
class func updateDownloadStatus(data:DownloadData) {
let sql = "update \(self.downloadsTable) set downloadsize = \(data.downloadsize),filesize = \(data.filesize) where url = '" + data.url.urlEncoded() + "'"
self.dbQueue?.inDatabase({ (db) in
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if !result! {
debugPrint("db update download falid!")
}
})
}
}
//MARK: - 說明書的數(shù)據(jù)庫操作
extension DBHelper {
/// 創(chuàng)建緩存的說明書表
class func createInstructionsCacheTable() {
self.dbQueue?.inDatabase({ (db) in
let sql = "create table if not exists \(self.instructionsCacheTable) (id integer primary key autoincrement,url text,title text,thumPath text,imgUrl text,tid integer,articletype integer,articeTitle text)"
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
//表創(chuàng)建成功
debugPrint("createInstructionsCacheTable sucess")
}else{
debugPrint("instructionsCacheTable create faild!")
MBProgressHUD.showFailImage("instructionsCacheTable create faild!")
}
})
}
/// 添加說明書下載
///
/// - Parameter download: 說明書信息
class func dbAddInstructionDownload(_ download:InstructionDownloadData){
self.dbQueue?.inDatabase({ (db) in
let imgPath = download.imgPath ?? ""
let imgUrl = download.imgUrl ?? ""
let sql = String.init(format: "insert into %@ (url,title,thumPath,imgUrl,tid,articletype,articeTitle) values ('%@','%@','%@','%@',%d,%d,'%@')", self.instructionsCacheTable,download.url.urlEncoded(),download.title.urlEncoded(),imgPath.urlEncoded(),imgUrl.urlEncoded(),download.tid ?? 0,download.articletype ?? 0,download.articeTitle ?? "unknown")
let result = db?.executeUpdate(sql, withArgumentsIn: nil)
if result! {
debugPrint("dbAddInstructionDownload - \(download.url)")
}else{
MBProgressHUD.showFailImage("db add instructionCache falid!")
debugPrint("db add instructionCache falid! - \(download.url)")
}
})
}
/// 查詢說明書的信息
///
/// - Parameters:
/// - url: 下載地主
/// - complete: 完成回調(diào)
class func dbSearchInstructionInfo(by url:String,complete:@escaping (InstructionDownloadData)->()) {
self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
if result.next(){
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")
inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")
complete(inst)
}
result.close()
})
}
}
這里我使用了閉包進(jìn)行數(shù)據(jù)的回傳,實際上更應(yīng)該通過返回值的方法,這樣能完全隔離數(shù)據(jù)庫操作。由于數(shù)據(jù)操作是同步執(zhí)行,所以不必?fù)?dān)心返回的時候數(shù)據(jù)庫還沒有執(zhí)行完導(dǎo)致數(shù)據(jù)不全或者為空的問題。
我們改寫一個方法:
class func dbSearchInstructionInfo(by url:String,complete:@escaping (InstructionDownloadData)->()) {
self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
if result.next(){
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")
inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")
complete(inst)
}
result.close()
})
}
改寫后:
class func dbSearchInstructionInfo(by url:String) -> InstructionDownloadData?{
var data:InstructionDownloadData? = nil
self.dbQueue?.inDatabase({ (db) in
let sql = String.init(format: "select * from %@ where url = '%@' ORDER BY id asc",self.instructionsCacheTable,url)
guard let result = db?.executeQuery(sql, withArgumentsIn: nil) else {
return
}
if result.next(){
let inst = InstructionDownloadData.init()
inst.imgPath = result.string(forColumn: "thumPath")
inst.imgUrl = result.string(forColumn: "imgUrl")
inst.tid = Int(result.int(forColumn: "tid"))
inst.articletype = Int(result.int(forColumn: "articletype"))
inst.articeTitle = result.string(forColumn: "articeTitle")
data = inst
}
result.close()
})
return data
}
從此類之外所有地方,都不在與數(shù)據(jù)庫進(jìn)行交互。