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

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

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

  • 目前移動端數(shù)據(jù)庫方案按其實現(xiàn)可分為兩類:
  1. 關(guān)系型數(shù)據(jù)庫,代表有CoreData痢艺、FMDB等。
  2. key-value數(shù)據(jù)庫介陶,代表有Realm堤舒、LevelDB、RocksDB等哺呜。
  • CoreData

它是蘋果內(nèi)建框架舌缤,和Xcode深度結(jié)合,可以很方便進行ORM某残;但其上手學(xué)習(xí)成本較高国撵,不容易掌握。穩(wěn)定性也堪憂玻墅,很容易crash介牙;多線程的支持也比較雞肋。

  • FMDB

它基于SQLite封裝澳厢,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說环础,簡單易懂囚似,可以直接上手;而缺點也正是在此线得,F(xiàn)MDB只是將SQLite的C接口封裝成了ObjC接口饶唤,沒有做太多別的優(yōu)化,即所謂的膠水代碼(Glue Code)贯钩。使用過程需要用大量的代碼拼接SQL搬素、拼裝Object,并不方便魏保。

因其在各平臺封裝熬尺、優(yōu)化的優(yōu)勢,比較受移動開發(fā)者的歡迎谓罗。對于iOS開發(fā)者粱哼,key-value的實現(xiàn)直接易懂,可以像使用NSDictionary一樣使用Realm檩咱。并且ORM徹底揭措,省去了拼裝Object的過程。但其對代碼侵入性很強刻蚯,Realm要求類繼承RLMObject的基類绊含。這對于單繼承的ObjC,意味著不能再繼承其他自定義的子類炊汹。同時躬充,key-value數(shù)據(jù)庫對較為復(fù)雜的查詢場景也比較無力。

  • 可見讨便,各個方案都有其獨特的優(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ā)者來說梳杏,上手倒是沒什么難度韧拒,但缺點顯而易見淹接,需要繼承,入侵性強叛溢,對于單繼承的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)計、復(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)化實踐
  • 損壞修復(fù):數(shù)據(jù)庫損壞一直是個難題礁鲁,WCDB內(nèi)置了我們自研的修復(fù)工具WCDBRepair。同樣可參考另一篇文章《微信 SQLite 數(shù)據(jù)庫修復(fù)實踐
  • 統(tǒng)計分析:WCDB提供接口直接獲取SQL的執(zhí)行耗時赁豆,可用于監(jiān)控性能仅醇。
  • 反注入:WCDB框架層防止了SQL注入,以避免惡意信息危害用戶數(shù)據(jù)魔种。

WCDB覆蓋了數(shù)據(jù)庫使用的絕大部分場景着憨,且經(jīng)過微信海量用戶的驗證,并將持續(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隨機數(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)致檢索速度變慢袱蚓,因為掃描索引節(jié)點的速度比全表掃描要慢钞啸。比如當(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ù)批量操作耗時來記錄的自娩,實際上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擴展的呀页。WCDB的查詢速度也還可以接受妈拌,這個結(jié)果其實跟其官方給出的結(jié)果差不多:讀操作基本等于FMDB速度,寫操作比FMDB快很多。

3. WCDB, FMDB性能對比

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

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

4.1 SQLite 優(yōu)缺點

優(yōu)點
  1. SQLite是輕量級的尘分,沒有客戶端和服務(wù)器端之分猜惋,并且是跨平臺的關(guān)系型數(shù)據(jù)庫。
  2. SQLite是一個單文件的培愁,可以copy出來在其他地方用著摔。
  3. 有一個SQLite.swift框架非常好用。
缺點
  1. SQLite在并發(fā)的讀寫方面性能不是很好定续,數(shù)據(jù)庫有時候可能會被某個讀寫操作獨占谍咆,可能會導(dǎo)致其他的讀寫操作被阻塞或者出錯。
  2. 不支持SQL92標(biāo)準(zhǔn)私股,有時候語法不嚴(yán)格也可以通過摹察,會養(yǎng)成不好習(xí)慣,導(dǎo)致不會維護倡鲸。
  3. 需要寫很多SQL拼接語句供嚎,寫很多膠水代碼,容易通過SQL注入惡意代碼峭状。
  4. 效率很低:SQL基于字符串克滴,命令行愛好者甚喜之。但對于基于現(xiàn)代IDE的移動開發(fā)者优床,卻是一大痛劝赔。字符串得不到任何編譯器的檢查,業(yè)務(wù)開發(fā)往往心中一團熱火羔巢,奮筆疾書下幾百行代碼望忆,滿心歡喜點下Run后才發(fā)現(xiàn):出錯了!靜心下來逐步看log竿秆、斷點后才發(fā)現(xiàn)启摄,噢,SELECT敲成SLEECT了幽钢。改正歉备,再等待編譯完成,此時已過去十幾分鐘匪燕。

4.2 FMDB 優(yōu)缺點

優(yōu)點
  1. 它基于SQLite封裝蕾羊,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說,
  2. 簡單易懂帽驯,可以直接上手龟再;
缺點
  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)缺點

優(yōu)點

它是蘋果內(nèi)建框架,和Xcode深度結(jié)合适荣,可以很方便進行ORM现柠;

缺點
  1. 其上手學(xué)習(xí)成本較高,不容易掌握束凑。
  2. 穩(wěn)定性也堪憂晒旅,很容易crash栅盲;多線程的支持也比較雞肋汪诉。

4.4 Realm優(yōu)缺點

優(yōu)點
  1. Realm在使用上和Core Data有點像,直接建立我們平常的對象Model類就是建立一個表了谈秫,確定主鍵扒寄、建立索引也在Model類里操作,幾行代碼就可以搞定拟烫,在操作上也可以很方便地增刪改查该编,不同于SQLite的SQL語句(即使用FMDB封裝的操作依然有點麻煩),Realm在日常使用上非常簡單硕淑,起碼在這次測試的例子中兩個數(shù)據(jù)庫同樣的一些操作课竣,Realm的代碼只有SQLite的一半。
  2. 其實Realm的“表”之間也可以建立關(guān)系置媳,對一于樟、對多關(guān)系都可以通過創(chuàng)建屬性來解決。
  3. 在.m方法中給“表”確定主鍵拇囊、屬性默認值迂曲、加索引的字段等。
  4. 修改數(shù)據(jù)時寥袭,可以直接丟進去一條數(shù)據(jù)路捧,Realm會根據(jù)主鍵判斷是否有這個數(shù)據(jù),有則更新传黄,沒有則添加杰扫。
  5. 查詢操作太簡單了,一行代碼根據(jù)查詢目的來獲取查詢結(jié)果的數(shù)組膘掰。
  6. 支持KVC和KVO章姓。
  7. 支出數(shù)據(jù)庫加密。
  8. 支持通知。
  9. 方便進行數(shù)據(jù)庫變更(版本迭代時可能發(fā)生表的新增啤覆、刪除苍日、結(jié)構(gòu)變化),Realm會自行監(jiān)測新增加和需要移除的屬性窗声,然后更新硬盤上的數(shù)據(jù)庫架構(gòu)相恃,Realm可以配置數(shù)據(jù)庫版本,進行判斷笨觅。
  10. 一般來說Realm比SQLite在硬盤上占用的空間更少拦耐。
缺點
  1. Realm也有一些限制,需要考慮是否會影響见剩。
  2. 類名長度最大57個UTF8字符杀糯。
  3. 屬性名長度最大63個UTF8字符。
  4. NSData及NSString屬性不能保存超過16M數(shù)據(jù)苍苞,如果有大的可以分塊固翰。
  5. 對字符串進行排序以及不區(qū)分大小寫查詢只支持“基礎(chǔ)拉丁字符集”、“拉丁字符補充集”羹呵、“拉丁文擴展字符集 A” 以及”拉丁文擴展字符集 B“(UTF-8 的范圍在 0~591 之間)骂际。
  6. 多線程訪問時需要新建新的Realm對象。
  7. Realm沒有自增屬性冈欢。歉铝。也就是說對于我們習(xí)慣的自增主鍵,如果確實需要凑耻,我們要自己去賦值太示,如果只要求獨一無二, 那么可以設(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ù)進行轉(zhuǎn)換存儲尉剩。
官方文檔

4.5 WCDB優(yōu)缺點

優(yōu)點
  1. 實際體驗后,WCDB的代碼體驗非常好毅臊,代碼量基本等于Realm理茎,都是SQLite的一半,
  2. 在風(fēng)格上比Realm更接近于OC原本的風(fēng)格管嬉,基本已經(jīng)感受不到是在寫數(shù)據(jù)庫的SQL操作皂林。并且其查詢語句WINQ也寫的很符合邏輯,基本都可以一看就懂蚯撩,甚至不需要你了解SQL語句础倍。
  3. 整個開發(fā)流程下來非常流暢,除了配置環(huán)境時出了問題并且沒有資料參考只能自己猜著解決外胎挎,代碼基本是一氣呵成寫完完美運行的沟启。
  4. WCDB通過ORM和WINQ,體現(xiàn)了其易用性上的優(yōu)勢呀癣,使得數(shù)據(jù)庫操作不再繁雜美浦。同時弦赖,通過鏈?zhǔn)秸{(diào)用项栏,開發(fā)者也能夠方便地獲取數(shù)據(jù)庫操作的耗時等性能信息。
  • 易用性
  1. one line of code 是它堅持的原則蹬竖,大多數(shù)操作只需要一行代碼即可完成.
  2. 使用WINQ 語句查詢沼沈,不用為拼接SQL語句而煩惱了,模型綁定映射也是按照規(guī)定模板去實現(xiàn)方便快捷币厕。
  • 高效性:上面已經(jīng)做過性能對比列另,WCDB對比其他框架效率和性能高很多。

  • 完整性

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

貼一份評論


貼一份評論
官方文檔

5. 總結(jié)

  1. 個人比較推薦使用微信的WCDB框架,這個框架是開源的呻袭,如果有需要一定要自己拼接SQL語句眨八,要實現(xiàn)SQL語句的擴展也是很容易的事情。
  2. 在選型上左电,每個框架都有自己的優(yōu)缺點廉侧,并沒有卻對的優(yōu)劣性页响,只有適不適合項目需求。其實對于小型項目直接使用Sqlite或者用FMDB 都可以滿足要求段誊,但是如果遇到安全性問題闰蚕,需要自己重復(fù)造很多輪子實現(xiàn)加密等功能。在使用上面如果直接使用SQL 語句连舍,像在Jimu1.0里面SQL語句到處散亂陪腌,出了問題不好定位,需要寫很多重復(fù)的拼接SQL語句的膠水代碼烟瞧。而且SQL語句如果寫錯了編譯并不會報錯或警告诗鸭,如果出現(xiàn)了因為SQL語句的bug,到項目后期很難定位。
  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ā)者來說专普,上手倒是沒什么難度悯衬,但缺點顯而易見,需要繼承檀夹,入侵性強筋粗,對于單繼承的OC來說這并不理想,而且對于集合類型不完全支持炸渡,復(fù)雜查詢也比較無力娜亿。
  4. WCDB是微信團隊于2017年6月9日開源的。開源時間不長蚌堵÷蚓觯可能相關(guān)資料比較少,只能靠查看官方文檔吼畏。
  5. SQLite3直接使用比較麻煩督赤,而FMDB是OC編寫的,如果使用Swift版本推薦使用:SQLite.swift這是很好的框架比使用FMDB簡單宫仗,代碼簡介很多够挂。SQLite.swift對SQLite進行了全面的封裝,擁有全面的純swift接口藕夫,即使你不會SQL語句孽糖,也可以使用數(shù)據(jù)庫枯冈。作者采用了鏈?zhǔn)骄幊痰膶懛ǎ寯?shù)據(jù)庫的管理變得優(yōu)雅办悟,可讀性也很強尘奏。
  • 綜合上述,我給出的建議是:
  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) // 插入失敗炫加,因為主鍵 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()
        //使用默認的目錄铺然,但是可以使用用戶名來替換默認的文件名
        config.fileURL = config.fileURL!.deletingLastPathComponent().appendingPathComponent("Bilibili.realm")
        //獲取我們的Realm文件的父級目錄
        let folderPath = config.fileURL!.deletingLastPathComponent().path
        //解除這個目錄的保護
        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 和表實例
            let realm = try! Realm()
            // 批量寫入操作
            realm.beginWrite()
            // add 方法支持 update 俗孝,item 的對象必須有主鍵
            for item in objects {
                realm.add(item, update: true)
            }
            // 提交寫入事務(wù)以確保數(shù)據(jù)在其他線程可用
            try! realm.commitWrite()
        }
    }
  • 后臺單獨進程寫入一組數(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 和表實例
                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 和表實例
            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進行了全面的封裝析恋,擁有全面的純swift接口良哲,即使你不會SQL語句,也可以使用數(shù)據(jù)庫助隧。作者采用了鏈?zhǔn)骄幊痰膶懛ㄖ欤寯?shù)據(jù)庫的管理變得優(yōu)雅,可讀性也很強喇颁。

  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ù)庫是否為只讀不可修改谒麦,默認為false。只讀的情況一般是我們復(fù)制一個數(shù)據(jù)庫文件到我們的項目哆致,只讀取數(shù)據(jù)使用绕德,不做修改。
  • 線程安全設(shè)置:

使用數(shù)據(jù)庫避免不了多線程操作摊阀,SQLite.swift中我們有兩個選項可以設(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
//計算year年month月type為1的所有value的和

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市胞此,隨后出現(xiàn)的幾起案子臣咖,更是在濱河造成了極大的恐慌,老刑警劉巖漱牵,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件亡哄,死亡現(xiàn)場離奇詭異,居然都是意外死亡布疙,警方通過查閱死者的電腦和手機蚊惯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來灵临,“玉大人截型,你說我怎么就攤上這事∪甯龋” “怎么了宦焦?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長顿涣。 經(jīng)常有香客問我波闹,道長,這世上最難降的妖魔是什么涛碑? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任精堕,我火速辦了婚禮,結(jié)果婚禮上蒲障,老公的妹妹穿的比我還像新娘歹篓。我一直安慰自己,他們只是感情好揉阎,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布庄撮。 她就那樣靜靜地躺著,像睡著了一般毙籽。 火紅的嫁衣襯著肌膚如雪洞斯。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天坑赡,我揣著相機與錄音烙如,去河邊找鬼么抗。 笑死,一個胖子當(dāng)著我的面吹牛厅翔,可吹牛的內(nèi)容都是我干的乖坠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼刀闷,長吁一口氣:“原來是場噩夢啊……” “哼熊泵!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甸昏,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤顽分,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后施蜜,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體卒蘸,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年翻默,在試婚紗的時候發(fā)現(xiàn)自己被綠了缸沃。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡修械,死狀恐怖趾牧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肯污,我是刑警寧澤翘单,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站蹦渣,受9級特大地震影響哄芜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜柬唯,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一认臊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧权逗,春花似錦美尸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽恕酸。三九已至堪滨,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蕊温,已是汗流浹背袱箱。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工遏乔, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人发笔。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓盟萨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親了讨。 傳聞我的和親對象是個殘疾皇子捻激,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容