@[TOC](IOS DB技術(shù)框架對比)
1. 數(shù)據(jù)庫簡介
- 目前移動端數(shù)據(jù)庫方案按其實(shí)現(xiàn)可分為兩類:
- 關(guān)系型數(shù)據(jù)庫,代表有CoreData、FMDB等。
- key-value數(shù)據(jù)庫,代表有Realm、LevelDB、RocksDB等。
- CoreData
它是蘋果內(nèi)建框架,和Xcode深度結(jié)合,可以很方便進(jìn)行ORM;但其上手學(xué)習(xí)成本較高,不容易掌握。穩(wěn)定性也堪憂,很容易crash;多線程的支持也比較雞肋。
- FMDB
它基于SQLite封裝,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說,簡單易懂,可以直接上手;而缺點(diǎn)也正是在此,F(xiàn)MDB只是將SQLite的C接口封裝成了ObjC接口,沒有做太多別的優(yōu)化,即所謂的膠水代碼(Glue Code)。使用過程需要用大量的代碼拼接SQL、拼裝Object,并不方便。
因其在各平臺封裝、優(yōu)化的優(yōu)勢,比較受移動開發(fā)者的歡迎。對于iOS開發(fā)者,key-value的實(shí)現(xiàn)直接易懂,可以像使用NSDictionary一樣使用Realm。并且ORM徹底,省去了拼裝Object的過程。但其對代碼侵入性很強(qiáng),Realm要求類繼承RLMObject的基類。這對于單繼承的ObjC,意味著不能再繼承其他自定義的子類。同時,key-value數(shù)據(jù)庫對較為復(fù)雜的查詢場景也比較無力。
- 可見,各個方案都有其獨(dú)特的優(yōu)勢及劣勢,沒有最好的,只有最適合的。
- 在選型上,F(xiàn)MDB的SQL拼接、難以防止的SQL注入;CoreData雖然可以方便ORM,但學(xué)習(xí)成本高,穩(wěn)定性堪憂,而且多線程雞肋;另外基于C語言的sqlite我想用的人也應(yīng)該不多;除了上述關(guān)系型數(shù)據(jù)庫之外然后還有一些其他的Key-Value型數(shù)據(jù)庫,如我用過的Realm,對于ObjC開發(fā)者來說,上手倒是沒什么難度,但缺點(diǎn)顯而易見,需要繼承,入侵性強(qiáng),對于單繼承的OC來說這并不理想,而且對于集合類型不完全支持,復(fù)雜查詢也比較無力。
- 下面介紹一下微信中使用的WCDB數(shù)據(jù)庫,它滿足了下面要求:
- 高效;增刪改查的高效是數(shù)據(jù)庫最基本的要求。除此之外,我們還希望能夠支持多個線程高并發(fā)地操作數(shù)據(jù)庫,以應(yīng)對微信頻繁收發(fā)消息的場景。
- 易用;這是微信開源的原則,也是WCDB的原則。SQLite本不是一個易用的組件:為了完成一個查詢,往往我們需要寫很多拼接字符串、組裝Object的膠水代碼。這些代碼冗長繁雜,而且容易出錯,我們希望組件能統(tǒng)一完成這些任務(wù)。
- 完整;數(shù)據(jù)庫操作是一個復(fù)雜的場景,我們希望數(shù)據(jù)庫組件能完整覆蓋各種場景。包括數(shù)據(jù)庫損壞、監(jiān)控統(tǒng)計(jì)、復(fù)雜的查詢、反注入等。
1.1 WCDB-iOS/Mac
WCDB-iOS/Mac(以下簡稱WCDB](https://github.com/Tencent/wcdb),均指代WCDB的iOS/Mac版本),是一個基于SQLite封裝的Objective-C++數(shù)據(jù)庫組件,提供了如下功能:
- 便捷的ORM和CRUD接口:通過WCDB,開發(fā)者可以便捷地定義數(shù)據(jù)庫表和索引,并且無須寫一坨膠水代碼拼裝對象。
- WINQ(WCDB語言集成查詢):通過WINQ,開發(fā)者無須拼接字符串,即可完成SQL的條件、排序、過濾等等語句。
-
多線程高并發(fā):基本的增刪查改等接口都支持多線程訪問,開發(fā)者無需操心線程安全問題。
- 線程間讀與讀、讀與寫操作均支持并發(fā)執(zhí)行。
- 寫與寫操作串行執(zhí)行,并且有基于SQLite源碼優(yōu)化的性能提升。可參考另一篇文章《微信iOS SQLite源碼優(yōu)化實(shí)踐》
- 損壞修復(fù):數(shù)據(jù)庫損壞一直是個難題,WCDB內(nèi)置了我們自研的修復(fù)工具WCDBRepair。同樣可參考另一篇文章《微信 SQLite 數(shù)據(jù)庫修復(fù)實(shí)踐》
- 統(tǒng)計(jì)分析:WCDB提供接口直接獲取SQL的執(zhí)行耗時,可用于監(jiān)控性能。
- 反注入:WCDB框架層防止了SQL注入,以避免惡意信息危害用戶數(shù)據(jù)。
WCDB覆蓋了數(shù)據(jù)庫使用的絕大部分場景,且經(jīng)過微信海量用戶的驗(yàn)證,并將持續(xù)不斷地增加新的能力。
具體WCDB使用可以參考這兩篇博客:
2. 數(shù)據(jù)庫 Realm、WCDB, SQLite性能對比
2.1 測試數(shù)據(jù)表結(jié)構(gòu)
Student表。
字段:ID、name、age、money。
| ID | name | age | money |
|---|---|---|---|
| 主鍵 | 姓名 | 年齡 | 存款(建索引) |
其中age為0100隨機(jī)數(shù)字,money為每一萬條數(shù)據(jù)中,010000各個數(shù)字只出現(xiàn)一次。
2.2 測試數(shù)據(jù)
對于以下測試數(shù)據(jù),只是給出一次測試后的具體數(shù)值供參考,經(jīng)過反復(fù)測試后的,基本都在這個時間量級上。
這里測試用的是純SQLite,沒有用FMDB。
2.2.1 SQLite3
- 9萬條數(shù)據(jù)基礎(chǔ)上連續(xù)單條插入一萬條數(shù)據(jù)耗時:1462ms。
- 已經(jīng)建立索引,需要注意的是,如果是檢索有大量重復(fù)數(shù)據(jù)的字段,不適合建立索引,反而會導(dǎo)致檢索速度變慢,因?yàn)閽呙杷饕?jié)點(diǎn)的速度比全表掃描要慢。比如當(dāng)我對age這個經(jīng)常重復(fù)的數(shù)據(jù)建立索引再對其檢索后,反而比不建立索引查詢要慢一倍多。
- 已經(jīng)設(shè)置WAL模式。
- 簡單查詢一萬次耗時:331ms
- dispatch 100個block來查詢一萬次耗時:150ms
2.2.2 realm
- 9萬條數(shù)據(jù)基礎(chǔ)上連續(xù)單條插入一萬條數(shù)據(jù)耗時:32851ms。
- 注意,Realm似乎必須通過事務(wù)來插入,所謂的單條插入即是每次都開關(guān)一次事務(wù),耗時很多,如果在一次事務(wù)中插入一萬條,耗時735ms。
- 已經(jīng)建立索引。
- 簡單查詢一萬次耗時:699ms。
- dispatch 100個block來查詢一萬次耗時:205ms。
2.2.3 WCDB
- 9萬條數(shù)據(jù)基礎(chǔ)上連續(xù)單條插入一萬條數(shù)據(jù)耗時:750ms。
- 此為不用事務(wù)操作的時間,如果用事務(wù)統(tǒng)一操作,耗時667ms。
- 已經(jīng)建立索引。
- 簡單查詢一萬次耗時:690ms。
- dispatch 100個block來查詢一萬次耗時:199ms。
2.2.4 三者對比
| 測試內(nèi)容 | Realm | WCDB | SQLite | 用例數(shù)量 |
|---|---|---|---|---|
| 單條插入一萬條 | 32851ms | 750ms | 1462ms | 90000+10000 |
| 循環(huán)查詢一萬次 | 699ms | 690ms | 331ms | 100000 |
| 100個block查詢一萬次 | 205ms | 199ms | 186ms | 100000 |
- 由于Realm單次事務(wù)操作一萬次耗時過長,圖表中顯示起來也就沒有了意義,因此下面圖中Realm的耗時是按照事務(wù)批量操作耗時來記錄的,實(shí)際上WCDB的插入操作是優(yōu)于Realm的。
- 從結(jié)果來看,Realm似乎必須用事務(wù),單條插入的性能會差很多,但是用事務(wù)來批量操作就會好一些。按照參考資料[3]中的測試結(jié)果,Realm在插入速度上比SQLite慢,比用FMDB快,而查詢是比SQLite快的。
- 而WCDB的表現(xiàn)很讓人驚喜,其插入速度非???,以至于比SQLite都快了一個量級,要知道WCDB也是基于SQLite擴(kuò)展的。WCDB的查詢速度也還可以接受,這個結(jié)果其實(shí)跟其官方給出的結(jié)果差不多:讀操作基本等于FMDB速度,寫操作比FMDB快很多。
3. WCDB, FMDB性能對比
4. 數(shù)據(jù)庫框架優(yōu)缺點(diǎn)對比
4.1 SQLite 優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- SQLite是輕量級的,沒有客戶端和服務(wù)器端之分,并且是跨平臺的關(guān)系型數(shù)據(jù)庫。
- SQLite是一個單文件的,可以copy出來在其他地方用。
- 有一個SQLite.swift框架非常好用。
缺點(diǎn)
- SQLite在并發(fā)的讀寫方面性能不是很好,數(shù)據(jù)庫有時候可能會被某個讀寫操作獨(dú)占,可能會導(dǎo)致其他的讀寫操作被阻塞或者出錯。
- 不支持SQL92標(biāo)準(zhǔn),有時候語法不嚴(yán)格也可以通過,會養(yǎng)成不好習(xí)慣,導(dǎo)致不會維護(hù)。
- 需要寫很多SQL拼接語句,寫很多膠水代碼,容易通過SQL注入惡意代碼。
- 效率很低:SQL基于字符串,命令行愛好者甚喜之。但對于基于現(xiàn)代IDE的移動開發(fā)者,卻是一大痛。字符串得不到任何編譯器的檢查,業(yè)務(wù)開發(fā)往往心中一團(tuán)熱火,奮筆疾書下幾百行代碼,滿心歡喜點(diǎn)下Run后才發(fā)現(xiàn):出錯了!靜心下來逐步看log、斷點(diǎn)后才發(fā)現(xiàn),噢,SELECT敲成SLEECT了。改正,再等待編譯完成,此時已過去十幾分鐘。
4.2 FMDB 優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 它基于SQLite封裝,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說,
- 簡單易懂,可以直接上手;
缺點(diǎn)
- FMDB只是將SQLite的C接口封裝成了ObjC接口,沒有做太多別的優(yōu)化,即所謂的膠水代碼(Glue Code)。
- 使用過程需要用大量的代碼拼接SQL、拼裝Object,并不方便。
- 容易通過SQL代碼注入。
- 直接暴露字符串接口,讓業(yè)務(wù)開發(fā)自己拼接字符串,取出數(shù)據(jù)后賦值給對應(yīng)的Object. 這種方式過于簡單粗暴。
官方文檔
4.3 CoreData 優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
它是蘋果內(nèi)建框架,和Xcode深度結(jié)合,可以很方便進(jìn)行ORM;
缺點(diǎn)
- 其上手學(xué)習(xí)成本較高,不容易掌握。
- 穩(wěn)定性也堪憂,很容易crash;多線程的支持也比較雞肋。
4.4 Realm優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- Realm在使用上和Core Data有點(diǎn)像,直接建立我們平常的對象Model類就是建立一個表了,確定主鍵、建立索引也在Model類里操作,幾行代碼就可以搞定,在操作上也可以很方便地增刪改查,不同于SQLite的SQL語句(即使用FMDB封裝的操作依然有點(diǎn)麻煩),Realm在日常使用上非常簡單,起碼在這次測試的例子中兩個數(shù)據(jù)庫同樣的一些操作,Realm的代碼只有SQLite的一半。
- 其實(shí)Realm的“表”之間也可以建立關(guān)系,對一、對多關(guān)系都可以通過創(chuàng)建屬性來解決。
- 在.m方法中給“表”確定主鍵、屬性默認(rèn)值、加索引的字段等。
- 修改數(shù)據(jù)時,可以直接丟進(jìn)去一條數(shù)據(jù),Realm會根據(jù)主鍵判斷是否有這個數(shù)據(jù),有則更新,沒有則添加。
- 查詢操作太簡單了,一行代碼根據(jù)查詢目的來獲取查詢結(jié)果的數(shù)組。
- 支持KVC和KVO。
- 支出數(shù)據(jù)庫加密。
- 支持通知。
- 方便進(jìn)行數(shù)據(jù)庫變更(版本迭代時可能發(fā)生表的新增、刪除、結(jié)構(gòu)變化),Realm會自行監(jiān)測新增加和需要移除的屬性,然后更新硬盤上的數(shù)據(jù)庫架構(gòu),Realm可以配置數(shù)據(jù)庫版本,進(jìn)行判斷。
- 一般來說Realm比SQLite在硬盤上占用的空間更少。
缺點(diǎn)
- Realm也有一些限制,需要考慮是否會影響。
- 類名長度最大57個UTF8字符。
- 屬性名長度最大63個UTF8字符。
- NSData及NSString屬性不能保存超過16M數(shù)據(jù),如果有大的可以分塊。
- 對字符串進(jìn)行排序以及不區(qū)分大小寫查詢只支持“基礎(chǔ)拉丁字符集”、“拉丁字符補(bǔ)充集”、“拉丁文擴(kuò)展字符集 A” 以及”拉丁文擴(kuò)展字符集 B“(UTF-8 的范圍在 0~591 之間)。
- 多線程訪問時需要新建新的Realm對象。
- Realm沒有自增屬性。。也就是說對于我們習(xí)慣的自增主鍵,如果確實(shí)需要,我們要自己去賦值,如果只要求獨(dú)一無二, 那么可以設(shè)為[[NSUUID UUID] UUIDString],如果還要求用來判斷插入的順序,那么可以用Date。
- Realm支持以下的屬性類型:BOOL、bool、int、NSInteger、long、long long、float、double、NSString、NSDate、NSData以及 被特殊類型標(biāo)記的NSNumber,注意,不支持集合類型,只有一個集合RLMArray,如果服務(wù)器傳來的有數(shù)組,那么需要我們自己取數(shù)據(jù)進(jìn)行轉(zhuǎn)換存儲。
官方文檔
4.5 WCDB優(yōu)缺點(diǎn)
優(yōu)點(diǎn)
- 實(shí)際體驗(yàn)后,WCDB的代碼體驗(yàn)非常好,代碼量基本等于Realm,都是SQLite的一半,
- 在風(fēng)格上比Realm更接近于OC原本的風(fēng)格,基本已經(jīng)感受不到是在寫數(shù)據(jù)庫的SQL操作。并且其查詢語句WINQ也寫的很符合邏輯,基本都可以一看就懂,甚至不需要你了解SQL語句。
- 整個開發(fā)流程下來非常流暢,除了配置環(huán)境時出了問題并且沒有資料參考只能自己猜著解決外,代碼基本是一氣呵成寫完完美運(yùn)行的。
- WCDB通過ORM和WINQ,體現(xiàn)了其易用性上的優(yōu)勢,使得數(shù)據(jù)庫操作不再繁雜。同時,通過鏈?zhǔn)秸{(diào)用,開發(fā)者也能夠方便地獲取數(shù)據(jù)庫操作的耗時等性能信息。
- 易用性
- one line of code 是它堅(jiān)持的原則,大多數(shù)操作只需要一行代碼即可完成.
- 使用WINQ 語句查詢,不用為拼接SQL語句而煩惱了,模型綁定映射也是按照規(guī)定模板去實(shí)現(xiàn)方便快捷。
高效性:上面已經(jīng)做過性能對比,WCDB對比其他框架效率和性能高很多。
完整性
- 支持基于SQLCipher 加密
- 持全文搜索
- 支持反注入,可以避免第三方從輸入框注入 SQL,進(jìn)行預(yù)期之外的惡意操作。
- 用戶不用手動管理數(shù)據(jù)庫字段版本,升級方便自動.
- 提供數(shù)據(jù)庫修復(fù)工具。
缺點(diǎn)
- 最明顯的缺點(diǎn)是其相關(guān)資料太少了
貼一份評論
官方文檔
5. 總結(jié)
- 個人比較推薦使用微信的WCDB框架,這個框架是開源的,如果有需要一定要自己拼接SQL語句,要實(shí)現(xiàn)SQL語句的擴(kuò)展也是很容易的事情。
- 在選型上,每個框架都有自己的優(yōu)缺點(diǎn),并沒有卻對的優(yōu)劣性,只有適不適合項(xiàng)目需求。其實(shí)對于小型項(xiàng)目直接使用Sqlite或者用FMDB 都可以滿足要求,但是如果遇到安全性問題,需要自己重復(fù)造很多輪子實(shí)現(xiàn)加密等功能。在使用上面如果直接使用SQL 語句,像在Jimu1.0里面SQL語句到處散亂,出了問題不好定位,需要寫很多重復(fù)的拼接SQL語句的膠水代碼。而且SQL語句如果寫錯了編譯并不會報(bào)錯或警告,如果出現(xiàn)了因?yàn)镾QL語句的bug,到項(xiàng)目后期很難定位。
- FMDB的SQL拼接、難以防止的SQL注入;CoreData雖然可以方便ORM,但學(xué)習(xí)成本高,穩(wěn)定性堪憂,而且多線程雞肋;另外基于C語言的sqlite我想用的人也應(yīng)該不多;除了上述關(guān)系型數(shù)據(jù)庫之外然后還有一些其他的Key-Value型數(shù)據(jù)庫,如我用過的Realm,對于ObjC開發(fā)者來說,上手倒是沒什么難度,但缺點(diǎn)顯而易見,需要繼承,入侵性強(qiáng),對于單繼承的OC來說這并不理想,而且對于集合類型不完全支持,復(fù)雜查詢也比較無力。
- WCDB是微信團(tuán)隊(duì)于2017年6月9日開源的。開源時間不長??赡芟嚓P(guān)資料比較少,只能靠查看官方文檔。
- SQLite3直接使用比較麻煩,而FMDB是OC編寫的,如果使用Swift版本推薦使用:SQLite.swift這是很好的框架比使用FMDB簡單,代碼簡介很多。SQLite.swift對SQLite進(jìn)行了全面的封裝,擁有全面的純swift接口,即使你不會SQL語句,也可以使用數(shù)據(jù)庫。作者采用了鏈?zhǔn)骄幊痰膶懛?,讓?shù)據(jù)庫的管理變得優(yōu)雅,可讀性也很強(qiáng)。
- 綜合上述,我給出的建議是:
- 最佳方案A是使用WCDB.swift框架。
- 方案B是使用SQLite.swift
- 方案C是使用 Realm 框架
- 方案D是使用 FMDB 框架
6. 簡單對比WCDB.swift,Realm.swift,SQLite.swift的用法
- 簡單對比WCDB.swift,Realm.swift,FMDB,SQLite.swift的用法
6.1 WCDB.swift基本用法
- 完整demo下載地址:WCDB.swift使用Demo
6.1.0 新建一個模型
import Foundation
import WCDBSwift
class Sample: TableCodable {
var identifier: Int? = nil
var description: String? = nil
enum CodingKeys: String, CodingTableKey {
typealias Root = Sample
static let objectRelationalMapping = TableBinding(CodingKeys.self)
case identifier
case description
static var columnConstraintBindings: [CodingKeys: ColumnConstraintBinding]? {
return [
identifier: ColumnConstraintBinding(isPrimary: true),
]
}
}
}
6.1.1 創(chuàng)建數(shù)據(jù)庫
private lazy var db : Database? = {
//1.創(chuàng)建數(shù)據(jù)庫
let docPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! + "/wcdb.db"
let database = Database(withPath: docPath + "/wcdb.db")
return database
}()
6.1.2 創(chuàng)建數(shù)據(jù)庫表
private func testCreateTable() {
guard let db = db else {
return
}
do {
//創(chuàng)建數(shù)據(jù)庫表
try db.create(table: TB_Sample, of: Sample.self)
} catch {
print(error)
}
}
6.1.3 插入操作
private func testInsert() {
guard let db = db else {
return
}
do {
//插入數(shù)據(jù)庫
let object = Sample()
object.identifier = 1
object.description = "insert"
try db.insert(objects: object, intoTable: TB_Sample) // 插入成功
try db.insert(objects: object, intoTable: TB_Sample) // 插入失敗,因?yàn)橹麈I identifier = 1 已經(jīng)存在
object.description = "insertOrReplace"
try db.insertOrReplace(objects: object, intoTable: TB_Sample) // 插入成功,且 description 的內(nèi)容會被替換為 "insertOrReplace"
} catch {
print(error)
}
}
6.1.4 刪除操作
private func testDelete() {
guard let db = db else {
return
}
do {
//刪除操作
// 刪除 sampleTable 中所有 identifier 大于 1 的行的數(shù)據(jù)
try db.delete(fromTable: TB_Sample,
where: Sample.Properties.identifier > 1)
// 刪除 sampleTable 中的所有數(shù)據(jù)
try db.delete(fromTable: TB_Sample)
} catch {
print(error)
}
}
6.1.5 更新操作
private func testUpdate() {
guard let db = db else {
return
}
do {
//更新數(shù)據(jù)
let object = Sample()
object.description = "update"
// 將 sampleTable 中前三行的 description 字段更新為 "update"
try db.update(table: TB_Sample,
on: Sample.Properties.description,
with: object,
limit: 3)
} catch {
print(error)
}
}
6.1.6 查詢操作
private func testQuery() {
guard let db = db else {
return
}
do {
//查詢操作
// 返回 sampleTable 中的所有數(shù)據(jù)
let allObjects: [Sample] = try db.getObjects(fromTable: TB_Sample)
print(allObjects)
// 返回 sampleTable 中 identifier 小于 5 或 大于 10 的行的數(shù)據(jù)
let objects: [Sample] = try db.getObjects(fromTable: TB_Sample,
where: Sample.Properties.identifier < 5 || Sample.Properties.identifier > 10)
print(objects)
// 返回 sampleTable 中 identifier 最大的行的數(shù)據(jù)
// let object: Sample? = try db.getObject(fromTable: TB_Sample,
// orderBy: Sample.Properties.identifier.asOrder(by: .descending))
// 獲取所有內(nèi)容
let allRows = try db.getRows(fromTable: TB_Sample)
print(allRows[row: 2, column: 0].int32Value) // 輸出 3
// 獲取第二行
let secondRow = try db.getRow(fromTable: TB_Sample, offset: 1)
print(secondRow[0].int32Value) // 輸出 2
// 獲取 description 列
let descriptionColumn = try db.getColumn(on: Sample.Properties.description, fromTable: TB_Sample)
print(descriptionColumn) // 輸出 "sample1", "sample1", "sample1", "sample2", "sample2"
// 獲取不重復(fù)的 description 列的值
let distinctDescriptionColumn = try db.getDistinctColumn(on: Sample.Properties.description, fromTable: TB_Sample)
print(distinctDescriptionColumn) // 輸出 "sample1", "sample2"
// 獲取第二行 description 列的值
let value = try db.getValue(on: Sample.Properties.description, fromTable: TB_Sample, offset: 1)
print(value.stringValue) // 輸出 "sample1"
// 獲取 identifier 的最大值
let maxIdentifier = try db.getValue(on: Sample.Properties.identifier.max(), fromTable: TB_Sample)
print(maxIdentifier.stringValue)
// 獲取不重復(fù)的 description 的值
let distinctDescription = try db.getDistinctValue(on: Sample.Properties.description, fromTable: TB_Sample)
print(distinctDescription.stringValue) // 輸出 "sample1"
} catch {
print(error)
}
}
6.2 Realm.swift基本用法
6.2.1 創(chuàng)建數(shù)據(jù)庫
import RealmSwift
static let sharedInstance = try! Realm()
static func initRealm() {
var config = Realm.Configuration()
//使用默認(rèn)的目錄,但是可以使用用戶名來替換默認(rèn)的文件名
config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("Bilibili.realm")
//獲取我們的Realm文件的父級目錄
let folderPath = config.fileURL!.deletingLastPathComponent().path
//解除這個目錄的保護(hù)
try! FileManager.default.setAttributes([FileAttributeKey.protectionKey: FileProtectionType.none], ofItemAtPath: folderPath)
//創(chuàng)建Realm
Realm.Configuration.defaultConfiguration = config
}
6.2.2 創(chuàng)建數(shù)據(jù)庫表
static func add<T: Object>(_ object: T) {
try! sharedInstance.write {
sharedInstance.add(object)
}
}
6.2.3 插入操作
/// 添加一條數(shù)據(jù)
static func addCanUpdate<T: Object>(_ object: T) {
try! sharedInstance.write {
sharedInstance.add(object, update: true)
}
}
- 添加一組數(shù)據(jù)
static func addListData<T: Object>(_ objects: [T]) {
autoreleasepool {
// 在這個線程中獲取 Realm 和表實(shí)例
let realm = try! Realm()
// 批量寫入操作
realm.beginWrite()
// add 方法支持 update ,item 的對象必須有主鍵
for item in objects {
realm.add(item, update: true)
}
// 提交寫入事務(wù)以確保數(shù)據(jù)在其他線程可用
try! realm.commitWrite()
}
}
- 后臺單獨(dú)進(jìn)程寫入一組數(shù)據(jù)
static func addListDataAsync<T: Object>(_ objects: [T]) {
let queue = DispatchQueue.global(qos: DispatchQoS.QoSClass.default)
// Import many items in a background thread
queue.async {
// 為什么添加下面的關(guān)鍵字,參見 Realm 文件刪除的的注釋
autoreleasepool {
// 在這個線程中獲取 Realm 和表實(shí)例
let realm = try! Realm()
// 批量寫入操作
realm.beginWrite()
// add 方法支持 update ,item 的對象必須有主鍵
for item in objects {
realm.add(item, update: true)
}
// 提交寫入事務(wù)以確保數(shù)據(jù)在其他線程可用
try! realm.commitWrite()
}
}
}
6.2.4 刪除操作
/// 刪除某個數(shù)據(jù)
static func delete<T: Object>(_ object: T) {
try! sharedInstance.write {
sharedInstance.delete(object)
}
}
6.2.5 更新操作
- 更新操作同添加操作,
/// 添加一條數(shù)據(jù)
static func addCanUpdate<T: Object>(_ object: T) {
try! sharedInstance.write {
sharedInstance.add(object, update: true)
}
}
static func addListData<T: Object>(_ objects: [T]) {
autoreleasepool {
// 在這個線程中獲取 Realm 和表實(shí)例
let realm = try! Realm()
// 批量寫入操作
realm.beginWrite()
// add 方法支持 update ,item 的對象必須有主鍵
for item in objects {
realm.add(item, update: true)
}
// 提交寫入事務(wù)以確保數(shù)據(jù)在其他線程可用
try! realm.commitWrite()
}
}
6.2.6 查詢操作
/// 根據(jù)條件查詢數(shù)據(jù)
static func selectByNSPredicate<T: Object>(_: T.Type , predicate: NSPredicate) -> Results<T>{
return sharedInstance.objects(T.self).filter(predicate)
}
/// 后臺根據(jù)條件查詢數(shù)據(jù)
static func BGselectByNSPredicate<T: Object>(_: T.Type , predicate: NSPredicate) -> Results<T>{
return try! Realm().objects(T.self).filter(predicate)
}
/// 查詢所有數(shù)據(jù)
static func selectByAll<T: Object>(_: T.Type) -> Results<T>{
return sharedInstance.objects(T.self)
}
/// 查詢排序后所有數(shù)據(jù),關(guān)鍵詞及是否升序
static func selectScoretByAll<T: Object>(_: T.Type ,key: String, isAscending: Bool) -> Results<T>{
return sharedInstance.objects(T.self).sorted(byKeyPath: key, ascending: isAscending)
}
6.3 FMDB基本用法
- FMDB的用法應(yīng)該比較熟悉,這里不論述
6.4 SQLite.swift基本用法
- 如果使用SQL語句的方式,推薦使用這個框架。
SQLite.swift對SQLite進(jìn)行了全面的封裝,擁有全面的純swift接口,即使你不會SQL語句,也可以使用數(shù)據(jù)庫。作者采用了鏈?zhǔn)骄幊痰膶懛ǎ寯?shù)據(jù)庫的管理變得優(yōu)雅,可讀性也很強(qiáng)。
- Swift的SQLite框架:SQLite.swift
- 集成:
- Carthage:
github "stephencelis/SQLite.swift"
- CocoaPods:
pod 'SQLite.swift'
- Swift Package Manager:
dependencies: [
.package(url: "https://github.com/stephencelis/SQLite.swift.git", from: "0.11.5")
]
6.4.1 創(chuàng)建數(shù)據(jù)庫
- 這里我們設(shè)置好數(shù)據(jù)庫文件的路徑和名稱,作為參數(shù)初始化一個Connection對象就可以了,如果路徑下文件不存在的話,會自動創(chuàng)建。
import SQLite
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
).first!
let db = try! Connection("\(path)/db.sqlite3")
- 初始化方法:
- 這只是最簡單的方式,我們來深入看一下Connection的初始化方法和可以設(shè)置的參數(shù):
public init(_ location: SQLite.Connection.Location = default, readonly: Bool = default) throws- 第一個參數(shù)Location指的是數(shù)據(jù)庫的位置,有三種情況:
inMemory數(shù)據(jù)庫存在內(nèi)存里;temporary臨時數(shù)據(jù)庫,使用完會被釋放掉;filename (or path)存在硬盤中,我們上面用的就是這種。前兩種使用完畢會被釋放不會保存,第三種可以保存下來;第一種數(shù)據(jù)庫存在內(nèi)存中,后兩種存在硬盤里。- readonly數(shù)據(jù)庫是否為只讀不可修改,默認(rèn)為false。只讀的情況一般是我們復(fù)制一個數(shù)據(jù)庫文件到我們的項(xiàng)目,只讀取數(shù)據(jù)使用,不做修改。
- 線程安全設(shè)置:
使用數(shù)據(jù)庫避免不了多線程操作,SQLite.swift中我們有兩個選項(xiàng)可以設(shè)置
db.busyTimeout = 5.0
db.busyHandler({ tries in
if tries >= 5 {
return false
}
return true
})
6.4.2 創(chuàng)建數(shù)據(jù)庫表
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
等價于執(zhí)行SQL:
// CREATE TABLE "users" (
// "id" INTEGER PRIMARY KEY NOT NULL,
// "name" TEXT,
// "email" TEXT NOT NULL UNIQUE
// )
此外還可以這樣創(chuàng)建:
let users = Table("users")
let id = Expression<Int64>("id")
let name = Expression<String?>("name")
let email = Expression<String>("email")
try db.run(users.create(temporary: false, ifNotExists: true, withoutRowid: false, block: { (t) in
t.column(id, primaryKey: true)
t.column(name)
t.column(email, unique: true)
})
)
/*
temporary:是否是臨時表
ifNotExists:是否不存在的情況才會創(chuàng)建,記得設(shè)置為true
withoutRowid: 是否自動創(chuàng)建自增的rowid
*/
6.4.3 插入操作
let insert = users.insert(name <- "Alice", email <- "alice@mac.com")
if let rowId = try? db.run(insert) {
print("插入成功:\(rowId)")
} else {
print("插入失敗")
}
//等價于執(zhí)行下面SQL
// INSERT INTO "users" ("name", "email") VALUES ('Alice', 'alice@mac.com')
插入成功會返回對應(yīng)的rowid
6.4.4 刪除操作
let alice = users.filter(id == rowid)
if let count = try? db.run(alice.delete()) {
print("刪除的條數(shù)為:\(count)")
} else {
print("刪除失敗")
}
//等價于執(zhí)行下面SQL
// DELETE FROM "users" WHERE ("id" = 1)
刪除成功會返回刪除的行數(shù)int值
6.4.5 更新操作
let alice = users.filter(id == rowid)
try db.run(alice.update(email <- email.replace("mac.com", with: "me.com")))
//等價于執(zhí)行下面SQL
// UPDATE "users" SET "email" = replace("email", 'mac.com', 'me.com')
// WHERE ("id" = 1)
//可以直接這樣
if let count = try? db.run(alice. update()) {
print("修改的條數(shù)為:\(count)")
} else {
print("修改失敗")
}
6.4.6 查詢操作
let query = users.filter(name == "Alice").select(email).order(id.desc).limit(l, offset: 1)
for user in try db.prepare(query) {
print("email: \(user[email])")
//email: alice@mac.com
}
for user in try db.prepare(users) {
print("id: \(user[id]), name: \(user[name]), email: \(user[email])")
// id: 1, name: Optional("Alice"), email: alice@mac.com
}
//等價于執(zhí)行下面SQL
// SELECT * FROM "users"
let stmt = try db.prepare("INSERT INTO users (email) VALUES (?)")
for email in ["betty@icloud.com", "cathy@icloud.com"] {
try stmt.run(email)
}
db.totalChanges // 3
db.changes // 1
db.lastInsertRowid // 3
for row in try db.prepare("SELECT id, email FROM users") {
print("id: \(row[0]), email: \(row[1])")
// id: Optional(2), email: Optional("betty@icloud.com")
// id: Optional(3), email: Optional("cathy@icloud.com")
}
try db.scalar("SELECT count(*) FROM users") // 2
6.4.7 封裝代碼
import UIKit
import SQLite
import SwiftyJSON
let type_column = Expression<Int>("type")
let time_column = Expression<Int>("time")
let year_column = Expression<Int>("year")
let month_column = Expression<Int>("month")
let week_column = Expression<Int>("week")
let day_column = Expression<Int>("day")
let value_column = Expression<Double>("value")
let tag_column = Expression<String>("tag")
let detail_column = Expression<String>("detail")
let id_column = rowid
class SQLiteManager: NSObject {
static let manager = SQLiteManager()
private var db: Connection?
private var table: Table?
func getDB() -> Connection {
if db == nil {
let path = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
).first!
db = try! Connection("\(path)/db.sqlite3")
db?.busyTimeout = 5.0
}
return db!
}
func getTable() -> Table {
if table == nil {
table = Table("records")
try! getDB().run(
table!.create(temporary: false, ifNotExists: true, withoutRowid: false, block: { (builder) in
builder.column(type_column)
builder.column(time_column)
builder.column(year_column)
builder.column(month_column)
builder.column(week_column)
builder.column(day_column)
builder.column(value_column)
builder.column(tag_column)
builder.column(detail_column)
})
)
}
return table!
}
//增
func insert(item: JSON) {
let insert = getTable().insert(type_column <- item["type"].intValue, time_column <- item["time"].intValue, value_column <- item["value"].doubleValue, tag_column <- item["tag"].stringValue , detail_column <- item["detail"].stringValue, year_column <- item["year"].intValue, month_column <- item["month"].intValue, week_column <- item["week"].intValue, day_column <- item["day"].intValue)
if let rowId = try? getDB().run(insert) {
print_debug("插入成功:\(rowId)")
} else {
print_debug("插入失敗")
}
}
//刪單條
func delete(id: Int64) {
delete(filter: rowid == id)
}
//根據(jù)條件刪除
func delete(filter: Expression<Bool>? = nil) {
var query = getTable()
if let f = filter {
query = query.filter(f)
}
if let count = try? getDB().run(query.delete()) {
print_debug("刪除的條數(shù)為:\(count)")
} else {
print_debug("刪除失敗")
}
}
//改
func update(id: Int64, item: JSON) {
let update = getTable().filter(rowid == id)
if let count = try? getDB().run(update.update(value_column <- item["value"].doubleValue, tag_column <- item["tag"].stringValue , detail_column <- item["detail"].stringValue)) {
print_debug("修改的結(jié)果為:\(count == 1)")
} else {
print_debug("修改失敗")
}
}
//查
func search(filter: Expression<Bool>? = nil, select: [Expressible] = [rowid, type_column, time_column, value_column, tag_column, detail_column], order: [Expressible] = [time_column.desc], limit: Int? = nil, offset: Int? = nil) -> [Row] {
var query = getTable().select(select).order(order)
if let f = filter {
query = query.filter(f)
}
if let l = limit {
if let o = offset{
query = query.limit(l, offset: o)
}else {
query = query.limit(l)
}
}
let result = try! getDB().prepare(query)
return Array(result)
}
}
- 封裝后使用更加方便
let inV = SQLiteManager.manager.search(filter: year_column == year && month_column == month && type_column == 1,
select: [value_column.sum]).first?[value_column.sum] ?? 0.0
//計(jì)算year年month月type為1的所有value的和