理论篇
本文默认你已经在项目中继承了FMDB,如果是swift项目,已经在swift中引入桥接文件并添加了FMDB。
iOS数据库常用的库有CoreData、FMDB、Realm等。对于三种方式的优缺点及速度等本文不做讨论,由于FMDB的受欢迎程度很高,本文对FMDB进行探讨和梳理。
首先三种数据库操作方式的简介:
- CoreData是Apple对sqlite的封装,一种面向对象的数据库操作方式。包含
.xcdatamodeld
的模型可视化操作,多表的关联,数据的变更检测等,功能强大。了解更多 认识CoreData-基础使用 - FMDB是开源的对sqlite的封装,使用sql语句进行对数据库的操作,灵活方便,本文主要讨论这个。了解更多SQLite的常见问题
- Realm是由Y Combinator孵化的创业团队开源出来的一款可以用于iOS(同样适用于Swift&Objective-C)和Android的跨平台移动数据库。目前最新版是Realm 2.0.2,支持的平台包括Java,Objective-C,Swift,React Native,Xamarin。提供可视化工具。Realm与sqlite无关。了解更多Realm数据库 从入门到“放弃”
FMDB主要类
- FMDatabase-代表一个独立的SQLite数据库,执行SQL语句。
- FMResultSet-代表FMDatebase查询的结果集,
- FMDatabaseQueue-如果你想要在多线程中查询和更新,你应该使用这个类,
FMDB创建数据库
FMDatabase通过一个SQLite数据库文件的路径创建。这个路径可以有以下三种样式:
- 一个系统文件路径.硬盘上之前不存在的,如果它不存在,FMDB会为你新建。
- 一个空字符串,一个空数据库在临时文件中创建。在数据库连接关闭的时候,这个数据库会被删除。
- NULL(空)。一个在内存中的数据库将被创建。在数据库连接关闭的时候,这个数据库会被销毁。
eg:
//OC
FMDatabase *db = [FMDatabase databaseWithpath:@"/tmp/tmp.db"];
//想了解更多关于临时或内存数据库,请阅读sqlite文档:http://www.sqlite.org/inmemorydb.html
//swift
let db = FMDatabase.init(path: "/tmp/tmp.db")
FMDB打开数据库
和数据库建立连接之前,应该确保它是打开的,在内存不足、禁止开启、创建数据库的时候会打开失败。
//OC
if(![db open]){
//打开数据库失败
return;
}
//Swift
if (db?.open())! {
//打开成功
}else{
//打开失败
}
FMDB执行更新
除了SELECT格式的数据库执行语句都是更新。包括CREATE,UPDATE,INSERT,ALTER,COMMIT,BEGIN,DETACH,DELETE,DROP,END,EXPLAIN,VACUUM,还有replace语句等等。基本上,只要你的SQL语句不是以SELECT开头,都是更新语句。
执行更新使用executeUpdate
方法,返回一个单一BOOL值,返回值为YES代表执行更新成功,返回值为NO表示发生了一些错误。你可以调用laseErrorMessage
和laseErrorCode
方法接收更多错误信息。
//OC
BOOL sucess = [db executeUpdate:@"DELETE FROM person WHERE person_id = ?",person.ID];
if sucess {
}else{
NSLog(@"执行出错了:%@",[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("执行出错了")
}
FMDB执行查询
一个SELECt语句是一个查询语句并且通过-executrQuery方法执行。
执行查询成功返回FMResultSet对象,失败返回nil。你应该使用laseErrorMessage
和laseErrorCode
方法确定到底为什么查询失败。
为了循环访问查询结果集,你可以使用while()
循环.你也需要从一个纪录到另一条。在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
}
通常你在使用查询结果的返回值前必须先调用FMResultSet
的next
方法,即使你只需要一次,像这样:
//OC
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:
UTF8StringForColumnName:
objectForColumnName:
其中时间date类型取出时使用dateForColumn
方法。
这里的每一个方法都有一个对应的{type}ForColumnIndex:表达式。基于字段在结果集中的位置可以被用来获取数据,和字段名一一对应。
特别的,你在这里不需要close
一个FMResultSet,直到结果集都被释放或者父数据库关闭了。
FMDB关闭(Close)
当你完成了数据的查询和更新,你应该close
这个FMDatabase的连接让SQLite释放那些操作过程中占用的资源。
FMDB 事务(Transactions)
FMDatabase可以通过调用合适的方法或者执行开始和结束事务型语句开始并提交一个事务。多条语句和批量添加你可以使用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中对于长字符串的处理,对每一段字符串用双引号"
括起来,可以换行,直到以分号;
结尾,表示字符串结束。
NSString *aa = @"长字符串的第一段"
"这是第二段"
"出现分号则表示字符串结束"
;
FMDB线程安全(FMDatabaseQueue)
在多线程使用同一个FMDatabase的单例是一个坏主意。通常为每一个线程创建FMDatabase对象都是OK的,但千万不要跨线程使用数据库单例。在多线程同时使用,你可能会遇到异常或闪退。
如果需要线程安全,用FMDatabaseQueue替代。
实例化一个FMDatabaseQueue的单例,并且在多线程中使用这个单例,将通过队列管理,同步执行来自多线程的命令。
这里是如何使用,第一步,创建你的队列。
//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() {
//获取数据并创建download对象
resultArray.append(download)
}
})
return resultArray
}
}
将简单的多任务包装在一个事务中,使用queue
的inTransaction
方法,当
//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将在队列中顺序执行代码块中的任务,因此你可以同时调用在多线程的多FMDatabaseQueue的方法,在顺序排到的时候它们将被执行。需要注意的是,在一个queue的block中,不能再次使用queue的block。
实战篇
实战场景
我们做一个app,这个app中可以下载网络内容,我们的下载管理界面中需要显示一些信息,那么大概需要记录下载任务的内容有:下载地址,下载文件名,文件大小,已下载大小,创建下载任务的源网页地址,创建时间。
我们自己也有个网站,是个说明书大全网站,因为说明书是由我们自己提供的内容,所以要能通过已下载的文件,分享这个文件的H5在线阅读的url地址,在第三方分享的时候要有封面图的远程url地址,并且在已下载文件里这个说明书可以进行“反馈”、“举报”。那么我们就需要知道这个说明书的“id”等信息。因此我们创建一个表单独记录这个说明书的信息。
在此功能完成中,需要“下载管理类”、“数据库管理类”、“沙盒文件管理类”相互配合。
其中通过文件的下载url作为关联标识符,以此url获取下载的信息,数据的信息,以及文件路径等信息。
下载管理类主要负责文件的下载,暂停,恢复,断点下载等;沙盒文件管理类主要负责下载文件的管理;数据库管理类负责记录下载的信息,以及其他需要持久化的信息。
因此关于下载后的文件路径地址,并不在数据库中存储,而是使用了另外的文件管理类,通过url地址等来获取相应的下载后的路径。此类不在此文探讨范围,暂且不表。
下载的步骤如下:
- 根据发起请求的header中的Content-Type检测打开的是个可下载文件。
- 创建下载任务,并在下载数据表中记录这个下载。
- 检查是否是检查网址是否来自自家网站,如果是,在说明书表中记录信息。
- 文件下载过程更新下载数据表中的信息。
显示步骤如下:
- 从下载数据表中查询所有内容,或者根据需求查询某些内容。
- 已下载文件的封面为各种格式的默认图,显示大小、时间等其他信息。
- 根据下载的url从说明书数据表中查询信息。
- 如果2没有找到说明书,不显示反馈、分享等工具栏,只显示文件的打开方式。
- 如果2有结果,则显示此下载关于说明书的封面图等其他信息。
实战代码
在项目应用中,个人推荐所有关于数据库的操作都放在一个类中。
通过swift的extension,将不同表的操作放在不同的区域,并且通过文档注释//MARK: -
的方法,建立快速索引。
这样就很容易管理数据库,并且此类以外所有地方都不在与数据库交互。
这里我们创建一个数据库管理类DBHelper
:
//
// DBManager.swift
// Browser
//
// Created by 西方 on 2018/1/24.
// Copyright © 2018年 114la.com. All rights reserved.
//
import UIKit
/// 数据库管理类,可在需要操作数据库的时候,通过扩展的模式添加方法,统一管理便于维护
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: - 下载表的数据库操作
extension DBHelper {
/// 创建下载表
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! {
//表创建成功
debugPrint("createDownloadsTable sucess")
}else{
debugPrint("downloads table create faild!")
MBProgressHUD.showFailImage("downloads table create faild!")
}
})
}
typealias DownloadResultCompleteHandle = ([DownloadData]) -> ()
/// 查询所有的下载数据
///
/// - Parameter complete: 完成回调
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是否已经下载过
///
/// - 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
}
/// 添加一个下载任务
///
/// - Parameters:
/// - url: 下载地址
/// - title: 任务的标题
class func dbAddDownload(_ url:String,webUrl:String,title:String){
self.dbQueue?.inDatabase({ (db) in
debugPrint("准备添加新的下载数据!")
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()))")
}
})
}
/// 更新下载的进度信息
///
/// - Parameter data: 下载数据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: - 说明书的数据库操作
extension DBHelper {
/// 创建缓存的说明书表
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! {
//表创建成功
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: 完成回调
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,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
}
从此类之外所有地方,都不在与数据库进行交互。