1. 程式人生 > IOS開發 >iOS FMDB遷移到WCDB

iOS FMDB遷移到WCDB

移動端的資料庫,除了使用"SQLite"這個共識,基本各自為政。

iOS這邊之前使用的是基於SQLite封裝的FMDB。一開始使用並無問題。但在長期的使用中反映出,有效能瓶頸,比如說某個使用者長期未登入,在登入時收到大量訊息,由於FMDB不支援多執行緒的寫操作,會導致寫入很慢。

遇到效能瓶頸後我們開始尋找FMDB的替代品,就是WCDB,微信開源官方移動端資料庫元件。進入我們的實現。依託微信的使用者量和對資料庫的依賴,WCDB已經處理了很多坑點和瓶頸,開源1年多,不斷地迭代功能,完善文件。同時WCDB在Github的wiki上提供了專門的教程,幫助使用FMDB的開發者進行遷移。

效能對比

對於已經上線執行的專案,解決效能瓶頸會是一個常見的遷移理由。相較於FMDB直白的封裝,WCDB上到OC層的ORM,下到SQLite原始碼,都做了各類效能優化。 為了驗證優化效果,微信提供benchmark,並將效能測試結果和測試程式碼上傳到了Github。同時,benchmark中也加入了FMDB的測試程式碼,用於橫向比較。 以下效能測試均為WAL模式、快取大小2000位元組、頁大小4 kb:

PRAGMA cache_size=-2000
PRAGMA page_size=4096
PRAGMA journal_mode=WAL
複製程式碼

測試資料均為含有一個整型和一個二進位制資料的表:CREATE TABLE benchmark(key INTEGER,value BLOB),二進位制資料長度為100位元組。

  • 讀操作效能測試
  • 寫操作效能測試
  • 批量寫操作效能測試 (事務)
    對於讀操作,SQLite速度很快,因此封裝層的消耗佔比較多。FMDB只做了最簡單的封裝, 而WCDB還包括ORM、WINQ等操作,因此執行的指令會比FMDB多,從而導致效能劣於FMDB 5%。 而寫操作通常是效能的瓶頸,WCDB對其做了許多針對性的優化,使得寫操作和批量寫操作的效能分別優於FMDB 28% 和 180%。
  • 多執行緒讀併發效能測試
  • 多執行緒讀寫併發效能測試
  • 多執行緒寫併發效能測試
    在多執行緒讀操作的測試中,WCDB多執行緒併發的優勢,將讀操作的效能劣勢拉了回來,使得最終結果與FMDB基本持平,而多執行緒讀寫操作效能則優於FMDB 62% 。 在多執行緒寫操作的測試中,FMDB直接返回錯誤SQLITE_BUSY,無法完成。
  • 初始化效能測試
    SQLite連線的初始化速度會隨著資料庫內表的數量增加而逐漸上升,WCDB也針對這個場景做了優化。相較於沒有優化的FMDB,WCDB 有107% 的效能優勢。

平滑遷移

檔案格式

由於FMDB和WCDB都基於SQLite,因此兩者在資料庫的檔案格式上一致。用FMDB建立、操作的資料庫,可以直接通過WCDB開啟、使用。因此開發者無需做額外的資料遷移。

表結構

WCDB提供了ORM的功能,將類的屬性繫結到資料庫表的欄位。在日常實踐中,類的屬性名和表的欄位名通常不一致。因此,WCDB提供了WCDB_SYNTHESIZE_COLUMN(className,propertyName,columnName)巨集,用於對映屬性名。 對於 表:CREATE TABLE message (db_id INTEGER,db_content TEXT) 類:

//Message.h
@interface Message : NSObject

@property int localID;

@property(retain) NSString *content;

@end

//Message.mm
@implementation Message

@end
複製程式碼

這裡表字段都加了"db_"的字首,並且使用了不一樣的欄位名。通過WCDB的ORM,可以對映為

//Message.h
@interface Message : NSObject <WCTTableCoding>

@property int localID;
@property(retain) NSString *content;
WCDB_PROPERTY(localID)
WCDB_PROPERTY(content)

@end
//Message.mm
@implementation Message

WCDB_IMPLEMENTATION(Message)
WCDB_SYNTHESIZE_COLUMN(Message,localID,"db_id")
WCDB_SYNTHESIZE_COLUMN(Message,content,"db_content")

@end
複製程式碼

通過WCDB_SYNTHESIZE_COLUMN巨集對映後,WCDB同樣能相容FMDB的表結構,開發者也不需要做資料遷移。由於WCDB較之FMDB效能上有著較大提升,遷移起來由於都是基於SQLite封裝,基本上都是相容的,所以我們決定使用WCDB。

替換前程式碼

+ (BOOL)insertGroupInfoData:(KitGroupInfoData *)infoData{
    BOOL result;
    //UPDATE
    KitGroupInfoData *groupInfoExit = [KitGroupInfoData getGroupInfoWithGroupId:infoData.groupId];
    if(groupInfoExit){//存在 updateHIYUNTON Group
        NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET groupName = ?,declared = ?,memberCount = ?,type = ?,isAnonymity = ?,isDiscuss = ? WHERE groupId = '%@' ",DATA_GROUPINFO_DBTABLE,infoData.groupId];
        result = [self updateTable:sql,!KCNSSTRING_ISEMPTY(infoData.groupName)? infoData.groupName:@"",!KCNSSTRING_ISEMPTY(infoData.declared)?infoData.declared:@"",[NSNumber numberWithInteger:infoData.memberCount],[NSNumber numberWithInteger:infoData.type],infoData.isAnonymity?@"1":@"0",infoData.isDiscuss?@"1":@"0"];
        return result;
    }else{//不存在insert
        NSString *sql = [NSString stringWithFormat:@"INSERT INTO %@ %@",@"(groupId,groupName,declared,createTime,owner,memberCount,type,isAnonymity,isDiscuss) VALUES (?,?,?)"];
        result = [self updateTable:sql,infoData.groupId,infoData.groupName,infoData.declared,infoData.createTime,infoData.owner,infoData.isDiscuss?@"1":@"0"];
        return result;
    }
    return YES;
}

+ (BOOL)upDateGroupInfo:(KitGroupInfoData *)groupInfo{
    __block BOOL result;//UPDATE
    [[[KitDataBaseManager sharedInstance] userDB_Queue] inDatabase:^(FMDatabase *db) {
        [db open];
        NSString *sql = [NSString stringWithFormat:@"UPDATE %@ SET groupName = ?,owner = ? WHERE groupId = '%@' ",groupInfo.groupId];
        result = [db executeUpdate:sql,!KCNSSTRING_ISEMPTY(groupInfo.groupName)? groupInfo.groupName:@"",!KCNSSTRING_ISEMPTY(groupInfo.declared)?groupInfo.declared:@"",groupInfo.owner];
        [db close];
    }];
    return result;
}
複製程式碼

替換後代碼

+ (BOOL)insertGroupInfoData:(KitGroupInfoData*)infoData{
    WCTDatabase *dataBase = [DataBaseManager sharedInstance].dataBase;
    return [dataBase insertOrReplaceObject:infoData into:DATA_GROUPINFO_DBTABLE];
}

+ (BOOL)upDateGroupInfo:(KitGroupInfoData *)groupInfo{
    WCTDatabase *dataBase = [DataBaseManager sharedInstance].dataBase;
    return [dataBase updateRowsInTable:DATA_GROUPINFO_DBTABLE onProperties:{KitGroupInfoData.groupName,KitGroupInfoData.declared,KitGroupInfoData.owner} withObject:groupInfo where:KitGroupInfoData.groupId == groupInfo.groupId];
}
複製程式碼

總結

在使用了WCDB之後,程式碼變得更加簡潔的同時,效能還得到了提高,也不需要額外關注資料庫升級和多執行緒操作的問題。WCDB還提供了加密、統計、修復等功能供我們使用。在解決效能瓶頸的同時,也解決之前使用FMDB的如下問題:

  1. 膠水程式碼的問題 過去一個幾十行的函式,絕大部分都是拼接SQL、處理SQLite返回的空資料和錯誤碼之類的“裹腳布”程式碼。而且這種程式碼四處分佈,字裡行間都寫著"Copy & Paste"。 而現在ORM取出即為物件無需拼接SQL。
  2. 效率問題 SQL基於字串,命令列愛好者甚喜之。但對於基於現代IDE的移動開發者,卻是一大痛。字串得不到任何編譯器的檢查,業務開發往往心中一團熱火,奮筆疾書下幾百行程式碼,滿心歡喜點下Run後才發現:出錯了!靜心下來逐步看log、斷點後才發現,噢,SELECT敲成SLEECT了。改正,再等待編譯完成,此時已過去十幾分鍾,還談何效率?而現在通過ORM式能夠通過IDE來檢測是否錯誤。
  3. SQL注入問題 SQL注入通常是利用SQL字串拼接的特點,用一些特殊符號提前截斷SQL,達到執行其他SQL的目的。試想這麼一段程式碼
    這段封裝很簡單,就是將訊息內容插入到資料庫中。假設對方發來這麼一條訊息:"');DELETE FROM message;--",那麼這條SQL就會被截斷成三部分:
    它會在插入一條訊息後,將表內的所有訊息刪除。倘若存在這樣的漏洞,後果將不堪設想。 其實反注入並不難,通過繫結引數或替換單引號為雙單引號即可解決。但要在業務開發的過程時時刻刻警惕這樣的風險,並不現實,畢竟人總會犯錯的。
  4. 多端同步問題 由於之前沒有安卓和iOS端通用的三方庫,大家也是各自為政的,使用不同自然會出現各種各樣的不同步問題。在接入了WCDB後,我們的資料庫方面便會統一

遷移過程中遇到的問題

  1. 工程中有的類是SDK提供的僅有標頭檔案。由於WCDB是基於物件繫結的,所以最後通過建立子類物件繫結WCDB做中間轉換實現。
  2. 專案中存了一個用於查詢運營商的db檔案,只是單純的從db裡查詢,同樣由於WCDB需要繫結無法實現,由於只是一個小查詢,使用位置不多,最後使用了sqlite3原生方法。

參考資料

為什麼要從FMDB遷移到WCDB?

github官方wiki