@[TOC](IOS DB技術(shù)框架對比)
1. 數(shù)據(jù)庫簡介
- 目前移動端數(shù)據(jù)庫方案按其實現(xiàn)可分為兩類:
- 關(guān)系型數(shù)據(jù)庫,代表有CoreData痢艺、FMDB等。
- 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使用可以參考這兩篇博客:
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的用踩。
- 從結(jié)果來看忙迁,Realm似乎必須用事務(wù)脐彩,單條插入的性能會差很多,但是用事務(wù)來批量操作就會好一些姊扔。按照參考資料[3]中的測試結(jié)果惠奸,Realm在插入速度上比SQLite慢,比用FMDB快恰梢,而查詢是比SQLite快的晨川。
- 而WCDB的表現(xiàn)很讓人驚喜,其插入速度非成静颍快共虑,以至于比SQLite都快了一個量級,要知道WCDB也是基于SQLite擴展的呀页。WCDB的查詢速度也還可以接受妈拌,這個結(jié)果其實跟其官方給出的結(jié)果差不多:讀操作基本等于FMDB速度,寫操作比FMDB快很多。
3. WCDB, FMDB性能對比
4. 數(shù)據(jù)庫框架優(yōu)缺點對比
4.1 SQLite 優(yōu)缺點
優(yōu)點
- SQLite是輕量級的尘分,沒有客戶端和服務(wù)器端之分猜惋,并且是跨平臺的關(guān)系型數(shù)據(jù)庫。
- SQLite是一個單文件的培愁,可以copy出來在其他地方用著摔。
- 有一個SQLite.swift框架非常好用。
缺點
- SQLite在并發(fā)的讀寫方面性能不是很好定续,數(shù)據(jù)庫有時候可能會被某個讀寫操作獨占谍咆,可能會導(dǎo)致其他的讀寫操作被阻塞或者出錯。
- 不支持SQL92標(biāo)準(zhǔn)私股,有時候語法不嚴(yán)格也可以通過摹察,會養(yǎng)成不好習(xí)慣,導(dǎo)致不會維護倡鲸。
- 需要寫很多SQL拼接語句供嚎,寫很多膠水代碼,容易通過SQL注入惡意代碼峭状。
- 效率很低: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)點
- 它基于SQLite封裝蕾羊,對于有SQLite和ObjC基礎(chǔ)的開發(fā)者來說,
- 簡單易懂帽驯,可以直接上手龟再;
缺點
- FMDB只是將SQLite的C接口封裝成了ObjC接口,沒有做太多別的優(yōu)化尼变,即所謂的膠水代碼(Glue Code)利凑。
- 使用過程需要用大量的代碼拼接SQL、拼裝Object,并不方便哀澈。
- 容易通過SQL代碼注入牌借。
- 直接暴露字符串接口,讓業(yè)務(wù)開發(fā)自己拼接字符串割按,取出數(shù)據(jù)后賦值給對應(yīng)的Object. 這種方式過于簡單粗暴膨报。
官方文檔
4.3 CoreData 優(yōu)缺點
優(yōu)點
它是蘋果內(nèi)建框架,和Xcode深度結(jié)合适荣,可以很方便進行ORM现柠;
缺點
- 其上手學(xué)習(xí)成本較高,不容易掌握束凑。
- 穩(wěn)定性也堪憂晒旅,很容易crash栅盲;多線程的支持也比較雞肋汪诉。
4.4 Realm優(yōu)缺點
優(yōu)點
- Realm在使用上和Core Data有點像,直接建立我們平常的對象Model類就是建立一個表了谈秫,確定主鍵扒寄、建立索引也在Model類里操作,幾行代碼就可以搞定拟烫,在操作上也可以很方便地增刪改查该编,不同于SQLite的SQL語句(即使用FMDB封裝的操作依然有點麻煩),Realm在日常使用上非常簡單硕淑,起碼在這次測試的例子中兩個數(shù)據(jù)庫同樣的一些操作课竣,Realm的代碼只有SQLite的一半。
- 其實Realm的“表”之間也可以建立關(guān)系置媳,對一于樟、對多關(guān)系都可以通過創(chuàng)建屬性來解決。
- 在.m方法中給“表”確定主鍵拇囊、屬性默認值迂曲、加索引的字段等。
- 修改數(shù)據(jù)時寥袭,可以直接丟進去一條數(shù)據(jù)路捧,Realm會根據(jù)主鍵判斷是否有這個數(shù)據(jù),有則更新传黄,沒有則添加杰扫。
- 查詢操作太簡單了,一行代碼根據(jù)查詢目的來獲取查詢結(jié)果的數(shù)組膘掰。
- 支持KVC和KVO章姓。
- 支出數(shù)據(jù)庫加密。
- 支持通知。
- 方便進行數(shù)據(jù)庫變更(版本迭代時可能發(fā)生表的新增啤覆、刪除苍日、結(jié)構(gòu)變化),Realm會自行監(jiān)測新增加和需要移除的屬性窗声,然后更新硬盤上的數(shù)據(jù)庫架構(gòu)相恃,Realm可以配置數(shù)據(jù)庫版本,進行判斷笨觅。
- 一般來說Realm比SQLite在硬盤上占用的空間更少拦耐。
缺點
- Realm也有一些限制,需要考慮是否會影響见剩。
- 類名長度最大57個UTF8字符杀糯。
- 屬性名長度最大63個UTF8字符。
- NSData及NSString屬性不能保存超過16M數(shù)據(jù)苍苞,如果有大的可以分塊固翰。
- 對字符串進行排序以及不區(qū)分大小寫查詢只支持“基礎(chǔ)拉丁字符集”、“拉丁字符補充集”羹呵、“拉丁文擴展字符集 A” 以及”拉丁文擴展字符集 B“(UTF-8 的范圍在 0~591 之間)骂际。
- 多線程訪問時需要新建新的Realm對象。
- Realm沒有自增屬性冈欢。歉铝。也就是說對于我們習(xí)慣的自增主鍵,如果確實需要凑耻,我們要自己去賦值太示,如果只要求獨一無二, 那么可以設(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ù)進行轉(zhuǎn)換存儲尉剩。
官方文檔
4.5 WCDB優(yōu)缺點
優(yōu)點
- 實際體驗后,WCDB的代碼體驗非常好毅臊,代碼量基本等于Realm理茎,都是SQLite的一半,
- 在風(fēng)格上比Realm更接近于OC原本的風(fēng)格管嬉,基本已經(jīng)感受不到是在寫數(shù)據(jù)庫的SQL操作皂林。并且其查詢語句WINQ也寫的很符合邏輯,基本都可以一看就懂蚯撩,甚至不需要你了解SQL語句础倍。
- 整個開發(fā)流程下來非常流暢,除了配置環(huán)境時出了問題并且沒有資料參考只能自己猜著解決外胎挎,代碼基本是一氣呵成寫完完美運行的沟启。
- WCDB通過ORM和WINQ,體現(xiàn)了其易用性上的優(yōu)勢呀癣,使得數(shù)據(jù)庫操作不再繁雜美浦。同時弦赖,通過鏈?zhǔn)秸{(diào)用项栏,開發(fā)者也能夠方便地獲取數(shù)據(jù)庫操作的耗時等性能信息。
- 易用性
- one line of code 是它堅持的原則蹬竖,大多數(shù)操作只需要一行代碼即可完成.
- 使用WINQ 語句查詢沼沈,不用為拼接SQL語句而煩惱了,模型綁定映射也是按照規(guī)定模板去實現(xiàn)方便快捷币厕。
高效性:上面已經(jīng)做過性能對比列另,WCDB對比其他框架效率和性能高很多。
完整性
- 支持基于SQLCipher 加密
- 持全文搜索
- 支持反注入旦装,可以避免第三方從輸入框注入 SQL页衙,進行預(yù)期之外的惡意操作。
- 用戶不用手動管理數(shù)據(jù)庫字段版本阴绢,升級方便自動.
- 提供數(shù)據(jù)庫修復(fù)工具店乐。
缺點
- 最明顯的缺點是其相關(guān)資料太少了
貼一份評論
官方文檔
5. 總結(jié)
- 個人比較推薦使用微信的WCDB框架,這個框架是開源的呻袭,如果有需要一定要自己拼接SQL語句眨八,要實現(xiàn)SQL語句的擴展也是很容易的事情。
- 在選型上左电,每個框架都有自己的優(yōu)缺點廉侧,并沒有卻對的優(yōu)劣性页响,只有適不適合項目需求。其實對于小型項目直接使用Sqlite或者用FMDB 都可以滿足要求段誊,但是如果遇到安全性問題闰蚕,需要自己重復(fù)造很多輪子實現(xiàn)加密等功能。在使用上面如果直接使用SQL 語句连舍,像在Jimu1.0里面SQL語句到處散亂陪腌,出了問題不好定位,需要寫很多重復(fù)的拼接SQL語句的膠水代碼烟瞧。而且SQL語句如果寫錯了編譯并不會報錯或警告诗鸭,如果出現(xiàn)了因為SQL語句的bug,到項目后期很難定位。
- 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ù)雜查詢也比較無力娜亿。
- WCDB是微信團隊于2017年6月9日開源的。開源時間不長蚌堵÷蚓觯可能相關(guān)資料比較少,只能靠查看官方文檔吼畏。
- SQLite3直接使用比較麻煩督赤,而FMDB是OC編寫的,如果使用Swift版本推薦使用:SQLite.swift這是很好的框架比使用FMDB簡單宫仗,代碼簡介很多够挂。SQLite.swift對SQLite進行了全面的封裝,擁有全面的純swift接口藕夫,即使你不會SQL語句孽糖,也可以使用數(shù)據(jù)庫枯冈。作者采用了鏈?zhǔn)骄幊痰膶懛ǎ寯?shù)據(jù)庫的管理變得優(yōu)雅办悟,可讀性也很強尘奏。
- 綜合上述,我給出的建議是:
- 最佳方案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) // 插入失敗炫加,因為主鍵 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)雅,可讀性也很強喇颁。
- 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ù)庫是否為只讀不可修改谒麦,默認為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的和