IOS DB技術(shù)框架對比

@[TOC](IOS DB技術(shù)框架對比)

1. 數(shù)據(jù)庫簡介

  • 目前移動端數(shù)據(jù)庫方案按其實(shí)現(xiàn)可分為兩類:
  1. 關(guān)系型數(shù)據(jù)庫,代表有CoreData、FMDB等。
  2. 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使用可以參考這兩篇博客:

  1. WCDB OC使用
  2. WCDB.swift使用

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的。
Realm、WCDB與SQLite移動數(shù)據(jù)庫性能對比測試

對比2
  • 從結(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性能對比

WCDB.swift和fmdb做對比
WCDB.swift和fmdb做對比

4. 數(shù)據(jù)庫框架優(yōu)缺點(diǎn)對比

4.1 SQLite 優(yōu)缺點(diǎn)

優(yōu)點(diǎn)
  1. SQLite是輕量級的,沒有客戶端和服務(wù)器端之分,并且是跨平臺的關(guān)系型數(shù)據(jù)庫。
  2. SQLite是一個單文件的,可以copy出來在其他地方用。
  3. 有一個SQLite.swift框架非常好用。
缺點(diǎn)
  1. SQLite在并發(fā)的讀寫方面性能不是很好,數(shù)據(jù)庫有時候可能會被某個讀寫操作獨(dú)占,可能會導(dǎo)致其他的讀寫操作被阻塞或者出錯。
  2. 不支持SQL92標(biāo)準(zhǔn),有時候語法不嚴(yán)格也可以通過,會養(yǎng)成不好習(xí)慣,導(dǎo)致不會維護(hù)。
  3. 需要寫很多SQL拼接語句,寫很多膠水代碼,容易通過SQL注入惡意代碼。
  4. 效率很低: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)
  1. 它基于SQLite封裝,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說,
  2. 簡單易懂,可以直接上手;
缺點(diǎn)
  1. FMDB只是將SQLite的C接口封裝成了ObjC接口,沒有做太多別的優(yōu)化,即所謂的膠水代碼(Glue Code)。
  2. 使用過程需要用大量的代碼拼接SQL、拼裝Object,并不方便。
  3. 容易通過SQL代碼注入。
  4. 直接暴露字符串接口,讓業(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)
  1. 其上手學(xué)習(xí)成本較高,不容易掌握。
  2. 穩(wěn)定性也堪憂,很容易crash;多線程的支持也比較雞肋。

4.4 Realm優(yōu)缺點(diǎn)

優(yōu)點(diǎn)
  1. Realm在使用上和Core Data有點(diǎn)像,直接建立我們平常的對象Model類就是建立一個表了,確定主鍵、建立索引也在Model類里操作,幾行代碼就可以搞定,在操作上也可以很方便地增刪改查,不同于SQLite的SQL語句(即使用FMDB封裝的操作依然有點(diǎn)麻煩),Realm在日常使用上非常簡單,起碼在這次測試的例子中兩個數(shù)據(jù)庫同樣的一些操作,Realm的代碼只有SQLite的一半。
  2. 其實(shí)Realm的“表”之間也可以建立關(guān)系,對一、對多關(guān)系都可以通過創(chuàng)建屬性來解決。
  3. 在.m方法中給“表”確定主鍵、屬性默認(rèn)值、加索引的字段等。
  4. 修改數(shù)據(jù)時,可以直接丟進(jìn)去一條數(shù)據(jù),Realm會根據(jù)主鍵判斷是否有這個數(shù)據(jù),有則更新,沒有則添加。
  5. 查詢操作太簡單了,一行代碼根據(jù)查詢目的來獲取查詢結(jié)果的數(shù)組。
  6. 支持KVC和KVO。
  7. 支出數(shù)據(jù)庫加密。
  8. 支持通知。
  9. 方便進(jìn)行數(shù)據(jù)庫變更(版本迭代時可能發(fā)生表的新增、刪除、結(jié)構(gòu)變化),Realm會自行監(jiān)測新增加和需要移除的屬性,然后更新硬盤上的數(shù)據(jù)庫架構(gòu),Realm可以配置數(shù)據(jù)庫版本,進(jìn)行判斷。
  10. 一般來說Realm比SQLite在硬盤上占用的空間更少。
缺點(diǎn)
  1. Realm也有一些限制,需要考慮是否會影響。
  2. 類名長度最大57個UTF8字符。
  3. 屬性名長度最大63個UTF8字符。
  4. NSData及NSString屬性不能保存超過16M數(shù)據(jù),如果有大的可以分塊。
  5. 對字符串進(jìn)行排序以及不區(qū)分大小寫查詢只支持“基礎(chǔ)拉丁字符集”、“拉丁字符補(bǔ)充集”、“拉丁文擴(kuò)展字符集 A” 以及”拉丁文擴(kuò)展字符集 B“(UTF-8 的范圍在 0~591 之間)。
  6. 多線程訪問時需要新建新的Realm對象。
  7. Realm沒有自增屬性。。也就是說對于我們習(xí)慣的自增主鍵,如果確實(shí)需要,我們要自己去賦值,如果只要求獨(dú)一無二, 那么可以設(shè)為[[NSUUID UUID] UUIDString],如果還要求用來判斷插入的順序,那么可以用Date。
  8. 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)
  1. 實(shí)際體驗(yàn)后,WCDB的代碼體驗(yàn)非常好,代碼量基本等于Realm,都是SQLite的一半,
  2. 在風(fēng)格上比Realm更接近于OC原本的風(fēng)格,基本已經(jīng)感受不到是在寫數(shù)據(jù)庫的SQL操作。并且其查詢語句WINQ也寫的很符合邏輯,基本都可以一看就懂,甚至不需要你了解SQL語句。
  3. 整個開發(fā)流程下來非常流暢,除了配置環(huán)境時出了問題并且沒有資料參考只能自己猜著解決外,代碼基本是一氣呵成寫完完美運(yùn)行的。
  4. WCDB通過ORM和WINQ,體現(xiàn)了其易用性上的優(yōu)勢,使得數(shù)據(jù)庫操作不再繁雜。同時,通過鏈?zhǔn)秸{(diào)用,開發(fā)者也能夠方便地獲取數(shù)據(jù)庫操作的耗時等性能信息。
  • 易用性
  1. one line of code 是它堅(jiān)持的原則,大多數(shù)操作只需要一行代碼即可完成.
  2. 使用WINQ 語句查詢,不用為拼接SQL語句而煩惱了,模型綁定映射也是按照規(guī)定模板去實(shí)現(xiàn)方便快捷。
  • 高效性:上面已經(jīng)做過性能對比,WCDB對比其他框架效率和性能高很多。

  • 完整性

  1. 支持基于SQLCipher 加密
  2. 持全文搜索
  3. 支持反注入,可以避免第三方從輸入框注入 SQL,進(jìn)行預(yù)期之外的惡意操作。
  4. 用戶不用手動管理數(shù)據(jù)庫字段版本,升級方便自動.
  5. 提供數(shù)據(jù)庫修復(fù)工具。
缺點(diǎn)
  1. 最明顯的缺點(diǎn)是其相關(guān)資料太少了

貼一份評論


貼一份評論
官方文檔

5. 總結(jié)

  1. 個人比較推薦使用微信的WCDB框架,這個框架是開源的,如果有需要一定要自己拼接SQL語句,要實(shí)現(xiàn)SQL語句的擴(kuò)展也是很容易的事情。
  2. 在選型上,每個框架都有自己的優(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)目后期很難定位。
  3. 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ù)雜查詢也比較無力。
  4. WCDB是微信團(tuán)隊(duì)于2017年6月9日開源的。開源時間不長??赡芟嚓P(guān)資料比較少,只能靠查看官方文檔。
  5. SQLite3直接使用比較麻煩,而FMDB是OC編寫的,如果使用Swift版本推薦使用:SQLite.swift這是很好的框架比使用FMDB簡單,代碼簡介很多。SQLite.swift對SQLite進(jìn)行了全面的封裝,擁有全面的純swift接口,即使你不會SQL語句,也可以使用數(shù)據(jù)庫。作者采用了鏈?zhǔn)骄幊痰膶懛?,讓?shù)據(jù)庫的管理變得優(yōu)雅,可讀性也很強(qiáng)。
  • 綜合上述,我給出的建議是:
  1. 最佳方案A是使用WCDB.swift框架。
  2. 方案B是使用SQLite.swift
  3. 方案C是使用 Realm 框架
  4. 方案D是使用 FMDB 框架

6. 簡單對比WCDB.swift,Realm.swift,SQLite.swift的用法

6.1 WCDB.swift基本用法

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)。

  1. Carthage:
github "stephencelis/SQLite.swift"
  1. CocoaPods:
pod 'SQLite.swift'
  1. 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")
  • 初始化方法:
  1. 這只是最簡單的方式,我們來深入看一下Connection的初始化方法和可以設(shè)置的參數(shù):
    public init(_ location: SQLite.Connection.Location = default, readonly: Bool = default) throws
  2. 第一個參數(shù)Location指的是數(shù)據(jù)庫的位置,有三種情況:
    inMemory數(shù)據(jù)庫存在內(nèi)存里;temporary臨時數(shù)據(jù)庫,使用完會被釋放掉;filename (or path)存在硬盤中,我們上面用的就是這種。前兩種使用完畢會被釋放不會保存,第三種可以保存下來;第一種數(shù)據(jù)庫存在內(nèi)存中,后兩種存在硬盤里。
  3. 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的和

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容