高效能Sqlite儲存模型物件解密

WHC發表於2017-01-02

前言

首先寫這篇文章之前祝大家元旦快樂,然後自我介紹一下,我叫吳海超(WHC)在iOS領域有豐富的開發架構經驗Github以後我也會以文章的形式分享具有實戰意義的文章給大家,希望能夠給大家有所幫助。

主題

好今天這篇文章我主要給大家講講Sqlite應用,我想大家應該都知道怎麼去應用以及使用Sqlite(建立資料庫,建表,增刪改查。。。),而且這些步驟現在都已經是固定模式了,既然都已經是固定模式了那我們可不可以對這些固定模式操作進行一層封裝讓使用更方便高效?答案是那是肯定可以的,這個具體怎麼封裝稍後再討論,我們先看看傳統使用Sqlite方式。

傳統方式使用Sqlite

  • 建立資料庫
sqlite3 *database;
- (void)createSqlite {
   NSArray *documentArray = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
   NSString *documentPath = [documentArray firstObject];
   // 新建名稱為whc.sqlite 資料檔案存放在沙箱doc目錄下面 
   NSString *path = [NSString stringWithFormat:@"%@/whc.sqlite",documentPath];
   /// 建立並且開啟資料庫
   sqlite3_open([[self path] UTF8String], &database);
}複製程式碼
  • 建表
- (void)createTable {
    const char *createSQL = "create table if not exists whc(id integer primary key autoincrement,name char,sex char)";
    sqlite3_exec(database, createSQL, NULL, NULL, NULL);
}複製程式碼
  • 增刪改查
//<1> 插入記錄Sql: "insert into whc (name,sex)values('whc','male')"
//<2> 刪除記錄Sql: "delete from whc where ...."
//<3> 修改記錄Sql: "update whc set ... where ...."
//<4> 查詢記錄Sql: "select from whc ......"
//執行sql:
sqlite3_stmt * stmt;
/// 預執行Sql
sqlite3_prepare_v2(database, sql, -1, &stmt, nil);
/// 執行sql
sqlite3_step(stmt);
.... 
處理結果,然後關閉資料庫複製程式碼

對傳統方式分析

我可以看到這上面的步驟是我們可以經常看到的方式,雖然上面的精簡了很多但是我認為還是很無聊,每建立一個資料庫都要這樣搞一遍效率實在很低而且不可靠一點都不靈活,其實大家都知道的CoreData也是很固定模式的,今年5月初哦不對現在是2017年了應該是去年5月,我就思考能不能一行程式碼運算元據庫呢?我下班在回去的公交車上思前想後覺得完全可行的,後來我的方案是:[Runtime + Sqlite]技術架構。

第二天就開始實施嘗試經過不斷除錯最後第一版本開發出來了WHC_ModelSqlite下面我看看怎麼來一行程式碼操作Sqlite

WHC_ModelSqlite介紹

  • 目標: 替代直接使用Sqlite和CoreData
  • 架構: 執行緒安全,採用runtime技術和Sqlite Api完美結合打造
  • 易用: 告別繁瑣sql語句的編寫和CoreData複雜建立
  • 支援: (NSData,NSString,Int,double,float,Bool,char,NSNumber)型別
  • 強大: 支援模型巢狀模型類儲存到資料庫和多表巢狀聯查
  • 智慧: 智慧根據資料庫模型類提供的VERSION方法返回的版本號動態更新資料庫欄位(動態刪除/新增)

先看看架構圖:

高效能Sqlite儲存模型物件解密

首先WHC_ModelSqlite是不需要顯示的去建立資料庫和表而是通過Model類來決定資料庫名稱和表名以及欄位名稱和資料型別,好下面先做一個使用演示:
假設有這樣一個模型Person類物件whc

Person * whc = [Person new];
whc.name = @"吳海超";
whc.age = 25;
whc.height = 180.0;
whc.weight = 140.0;
whc.isDeveloper = YES;
whc.sex = 'm';

// 巢狀car物件
whc.car = [Car new];
whc.car.name = @"撼路者";
whc.car.brand = @"大路虎";

// 巢狀school物件
whc.school = [School new];
whc.school.name = @"北京大學";
whc.school.personCount = 5000;

// school物件巢狀city物件
whc.school.city = [City new];
whc.school.city.name = @"北京";
whc.school.city.personCount = 1000;複製程式碼

我們怎麼把person插入到資料庫呢?看下面

/// 把上面建立好的whc物件插入資料庫
[WHC_ModelSqlite insert:whc];複製程式碼

可能很多人在這裡會有疑問?怎麼沒有建立資料庫和表以及欄位呢?首先WHC_ModelSqlite上面介紹以及說了是不需要這樣操作的,為什麼?那是因為WHC_ModelSqlite是基於Runtime技術開發的在上面執行[WHC_ModelSqlite insert:whc]時候會檢查模型Person類是否已經建立了資料庫,如果沒有會為Person模型類建立,然後通過Runtime讀取Person的屬性名稱以及型別然後通過型別對映把oc資料型別轉換為Sqlite欄位型別並且建立表(表名就是model類名),好上面對建立資料庫表以及欄位做了分析,那麼whc物件是如何插入到資料庫的?
在上面的檢查工作都好做了insert會用runtime技術獲取model物件whc所有屬性名稱型別以及對應的值,然後通過前面獲取的屬性名稱和值拼接Sql插入語句然後再執行的,對沒錯步驟就是這樣。然後過程看起來好像很簡單但其實有很多細節要做控制,具體看參考原始碼
插入程式碼片段

+ (void)insert:(id)model_object {
    /// 如果當前有其他資料庫的操作那麼等待
    dispatch_semaphore_wait([self shareInstance].dsema, DISPATCH_TIME_FOREVER);
    @autoreleasepool {
        [[self shareInstance].sub_model_info removeAllObjects];
        [self insertModelObject:model_object];
    }
    dispatch_semaphore_signal([self shareInstance].dsema);
}

/// 處理插入模型物件
+ (sqlite_int64)insertModelObject:(id)model_object {
    /// 掃描model物件的詳細資訊(runtime)
    NSDictionary * sub_model_objects_info = [self scanSubModelObject:model_object];
    if (sub_model_objects_info.count > 0) {
        [sub_model_objects_info enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
            /// 分析處理model物件的子model物件
            sqlite_int64 _id = [self insertModelObject:obj];
            [[self shareInstance].sub_model_info setObject:@(_id) forKey:key];
        }];
    }
    /// 插入model物件並且返回_id
    return [self commonInsertSubModelObject:model_object];
}

+ (sqlite_int64)commonInsertSubModelObject:(id)sub_model_object {
    sqlite_int64 _id = -1;
    /// 建立表以及開啟表建立欄位
    if ([self openTable:[sub_model_object class]]) {
       /// 開啟事務
        [self execSql:@"BEIGIN"];
        /// 插入model物件
        [self commonInsert:sub_model_object index:-1];
        /// 提交事務
        [self execSql:@"COMMIT"];
        /// 獲取表最大的id並返回
        _id = [self getModelMaxIdWithClass:[sub_model_object class]];
        /// 關閉資料庫
        [self close];
    }
    return _id;
}

/// 執行插入操作
+ (void)commonInsert:(id)model_object index:(NSInteger)index {
    sqlite3_stmt * pp_stmt = nil;
    /// 分析model資訊(屬性名稱/型別)
    NSDictionary * field_dictionary = [self parserModelObjectFieldsWithModelClass:[model_object class]];
    /// 獲取表名
    NSString * table_name = NSStringFromClass([model_object class]);
    /// 下面都是構建sql insert插入語句模組
    __block NSString * insert_sql = [NSString stringWithFormat:@"INSERT INTO %@ (",table_name];
    NSArray * field_array = field_dictionary.allKeys;
    NSMutableArray * value_array = [NSMutableArray array];
    NSMutableArray * insert_field_array = [NSMutableArray array];
    [field_array enumerateObjectsUsingBlock:^(NSString *  _Nonnull field, NSUInteger idx, BOOL * _Nonnull stop) {
        WHC_PropertyInfo * property_info = field_dictionary[field];
        [insert_field_array addObject:field];
        insert_sql = [insert_sql stringByAppendingFormat:@"%@,",field];
        id value = [model_object valueForKey:field];
        id subModelKeyId = [self shareInstance].sub_model_info[property_info.name];
        if ((value && subModelKeyId == nil) || index == _NO_HANDLE_KEY_ID) {
            [value_array addObject:value];
        }else {
            switch (property_info.type) {
                case _Data: {
                    [value_array addObject:[NSData data]];
                }
                    break;
                case _String: {
                    [value_array addObject:@""];
                }
                    break;
                case _Number: {
                    [value_array addObject:@(0.0)];
                }
                    break;
                case _Model: {
                    if ([subModelKeyId isKindOfClass:[NSArray class]]) {
                        [value_array addObject:subModelKeyId[index]];
                    }else {
                        if (subModelKeyId) {
                            [value_array addObject:subModelKeyId];
                        }else {
                            [value_array addObject:@(_NO_HANDLE_KEY_ID)];
                        }
                    }
                }
                    break;
                case _Int: {
                    id sub_model_main_key_object = [self shareInstance].sub_model_info[property_info.name];
                    if (sub_model_main_key_object != nil) {
                        if (index != -1) {
                            [value_array addObject:sub_model_main_key_object[index]];
                        }else {
                            [value_array addObject:sub_model_main_key_object];
                        }
                    }else {
                        NSNumber * value = @(((int64_t (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                        [value_array addObject:value];
                    }
                }
                    break;
                case _Boolean: {
                    NSNumber * value = @(((Boolean (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Char: {
                    NSNumber * value = @(((int8_t (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Double: {
                    NSNumber * value = @(((double (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                case _Float: {
                    NSNumber * value = @(((float (*)(id, SEL))(void *) objc_msgSend)((id)model_object, property_info.getter));
                    [value_array addObject:value];
                }
                    break;
                default:
                    break;
            }
        }
    }];

    insert_sql = [insert_sql substringWithRange:NSMakeRange(0, insert_sql.length - 1)];
    insert_sql = [insert_sql stringByAppendingString:@") VALUES ("];

    [field_array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        insert_sql = [insert_sql stringByAppendingString:@"?,"];
    }];
    insert_sql = [insert_sql substringWithRange:NSMakeRange(0, insert_sql.length - 1)];
    insert_sql = [insert_sql stringByAppendingString:@")"];

    /// 準備執行插入sql
    if (sqlite3_prepare_v2(_whc_database, [insert_sql UTF8String], -1, &pp_stmt, nil) == SQLITE_OK) {
        [field_array enumerateObjectsUsingBlock:^(NSString *  _Nonnull field, NSUInteger idx, BOOL * _Nonnull stop) {
        /// 進行值得繫結
            WHC_PropertyInfo * property_info = field_dictionary[field];
            id value = value_array[idx];
            int index = (int)[insert_field_array indexOfObject:field] + 1;
            switch (property_info.type) {
                case _Data:
                    sqlite3_bind_blob(pp_stmt, index, [value bytes], (int)[value length], SQLITE_TRANSIENT);
                    break;
                case _String:
                    sqlite3_bind_text(pp_stmt, index, [value UTF8String], -1, SQLITE_TRANSIENT);
                    break;
                case _Number:
                    sqlite3_bind_double(pp_stmt, index, [value doubleValue]);
                    break;
                case _Model:
                    sqlite3_bind_int64(pp_stmt, index, (sqlite3_int64)[value integerValue]);
                    break;
                case _Int:
                    sqlite3_bind_int64(pp_stmt, index, (sqlite3_int64)[value longLongValue]);
                    break;
                case _Boolean:
                    sqlite3_bind_int(pp_stmt, index, [value boolValue]);
                    break;
                case _Char:
                    sqlite3_bind_int(pp_stmt, index, [value intValue]);
                    break;
                case _Float:
                    sqlite3_bind_double(pp_stmt, index, [value floatValue]);
                    break;
                case _Double:
                    sqlite3_bind_double(pp_stmt, index, [value doubleValue]);
                    break;
                default:
                    break;
            }
        }];
        if (sqlite3_step(pp_stmt) != SQLITE_DONE) {
            sqlite3_finalize(pp_stmt);
        }
    }else {
        [self log:@"Sorry儲存資料失敗,建議檢查模型類屬性型別是否符合規範"];
    }
}複製程式碼

好上面對開源庫WHC_ModelSqlite的插入操作做了詳細分析,由於篇幅限制下面我其他操作簡要進行演示。

查詢

/// 查詢所有記錄
NSArray * personArray = [WHC_ModelSqlite query:[Person class]];
/// 查詢name欄位為whc並且age等於25的小夥子
[WHC_ModelSqlite query:[Person class] where:@"name = 'whc' and age = 25"];
///對person資料表查詢並且根據age自動降序或者升序排序
[WHC_ModelSqlite query:[Person class] order:@"by age desc/asc"];
/// 對person資料表查詢並且並且限制查詢數量為8
[WHC_ModelSqlite query:[Person class] limit:@"8"];
/// 對person資料表查詢並且對查詢列表偏移8並且限制查詢數量為8
[WHC_ModelSqlite query:[Person class] limit:@"8 offset 8"];複製程式碼

上面api具體程式碼實現可以參考原始碼

更新

/// 把記錄name等於whc或者age小於等於30的更新為現在的模型whc物件
[WHC_ModelSqlite update:whc where:@"name = 'whc' OR age <= 30"];複製程式碼

有沒有很先進。。。。。perfect

刪除

/// 刪除記錄age等於25並且name等於whc的記錄
[WHC_ModelSqlite delete:[Person class] where:@"age = 25 AND name = 'whc'"];複製程式碼

清空

/// 清空資料庫Person所有記錄
[WHC_ModelSqlite clear:[Person class]];複製程式碼

刪除資料庫

/// 刪除Person資料庫
[WHC_ModelSqlite removeModel:[Person class]];複製程式碼

刪除所有資料庫

/// 刪除所有模型資料庫
[WHC_ModelSqlite removeAllModel];複製程式碼

獲取資料庫本地路徑

NSString * path = [WHC_ModelSqlite localPathWithModel:[Person class]];複製程式碼

獲取資料庫本地版本號

NSString * path = [WHC_ModelSqlite versionWithModel:[Person class]];複製程式碼

注意

當模型類有新增/刪除屬性的時候需要在模型類裡定義類方法(+ (NSString*)VERSION)修改模型類(資料庫)版本號來表明有欄位更新操作,庫會根據這個VERSION變更智慧檢查並自動更新資料庫欄位,無需手動更新資料庫欄位。

結束

  • 如果您在使用過程中有任何問題,歡迎issue me! - 很樂意為您解答任何相關問題!
  • 與其給我點star,不如向我狠狠地拋來一個BUG!
  • 如果您想要更多的介面來自定義或者建議/意見,歡迎issue me!我會根據大家的需求提供更多的介面!

謝謝您的閱讀,WHC_ModelSqlite開源地址:github.com/netyouli/WH…
本人其他優秀開源專案Github

相關文章