從0開始弄一個面向OC資料庫(四)–複雜資料模型儲存

張書康發表於2019-01-01

前言

在前面的三個階段,我們分別實現的功能:

總之: 我們實現了面向模型的資料庫增刪查改,以及資料庫升級。感覺功能實現得差不多了,但是如果存得模型得成員變數裡面包含了另外得模型或者陣列、字典,那麼我們就沒法存了。我們要解決他,這就是本篇要做的。

從0開始弄一個面向OC資料庫(四)–複雜資料模型儲存

本篇我們要實現:複雜資料型別的儲存,比如自定義物件、陣列、字典等……然後我們還要實現模型巢狀模型,陣列、字典巢狀模型以及各種相互巢狀的情況。 本篇思路有點繞,需要沉著冷靜並且實踐才行。先看一下我們最終實現的結果,我們向資料庫記憶體儲一個非常複雜的模型:

插入資料庫

插入資料庫是成功的,但是插入成功不重要,重要的是,你取出來的時候,他是不是插入之前的樣子,下面我們進行資料庫查詢,得到以下結果:

從0開始弄一個面向OC資料庫(四)–複雜資料模型儲存

從得到的結果來看,各種型別巢狀的模型,我們能過完美的插入資料庫,同時,我們也能完美將它從資料庫取出來並且還原為模型,灰常的牛逼。這個一定是我們比FMDB,Realm這種航母級別的優勢所在。你是不是迫不及待的想知道這是如何實現的?下面會一一講解。

功能實現

在實現功能之前,我們一定要先考慮一下實現方式,考慮好了再開始動手,先看一下前輩們是如何做的,看過之後我們總結出兩個方式:

  • 一種是將不管三七二十一通通轉成NSData,轉不了的就歸檔轉!然後存,取資料的時候就解檔取。
  • 另外就是通通轉成字串,有一些可以直接轉成JSON字串,不能直接轉的通過一定的規律轉成字串,取的時候轉回去就取就好了。

最終我們使用第二種方式。接下來,我們逐條實現對應的功能,過程會比較繞邏輯,講得不太明白的建議直接看程式碼,反正是繞了我挺久的?。

1、模型轉字串

這個過程我們要先把模型轉成字典,然後在將字典轉成字串。
先來一種非常簡單的情況,比如下面這個模型裡面只有兩個基本資料型別的成員變數:

@interface School : NSObject
@property (nonatomic,copy) NSString *name; // 名字 (值:清華大學) 
@property (nonatomic,assign) NSInteger schoolId; // 學校id (值:1)
@end
複製程式碼

我們首先將他轉成以下字典格式:

{
    name = "清華大學";
    schoolId = 1;
}
複製程式碼

思路1:首先取模型所有成員變數,根據成員變數的名字通過KVC從模型中取值,以School的第一個成員變數name為例,我們根據成員變數的名字(name)通過KVC從模型中取值(id型別的@“清華大學”),然後根據成員變數的型別(name欄位對應的型別為NSString)將值轉換成對應型別的值,以成員變數的名字(name)為字典key,值(@”清華大學”)為字典value,逐條組成字典。

實現: 當然還有模型巢狀模型的情況,這種情況就在思路1的加黑部分取處理,首先從大的模型裡逐個成員變數轉換到字典內,如果當型別是模型,那麼我們先將裡面這個模型轉成字串再存到字典內,也就是重複以上步驟了,類似遞迴,說得可能有點繞,直接上程式碼了。

#pragma mark 模型轉字典
+ (NSDictionary *)dictWithModel:(id)model {
    // 獲取類的所有成員變數的名稱與型別 {name : NSString}
    NSDictionary *nameTypeDict = [CWModelTool classIvarNameAndTypeDic:[model class]];
    // 獲取模型所有成員變數 @[name,schollId]
    NSArray *allIvarNames = nameTypeDict.allKeys;
    
    NSMutableDictionary *allIvarValues = [NSMutableDictionary dictionary];
    // 獲取所有成員變數對應的值
    for (NSString *ivarName in allIvarNames) {
        id value = [model valueForKeyPath:ivarName];
        NSString *type = nameTypeDict[ivarName];
        
        value = [CWModelTool formatModelValue:value type:type isEncode:YES];
        allIvarValues[ivarName] = value;
    }
    return allIvarValues;
}

#pragma mark - 格式化欄位資料,我們的宗旨:一切不可識別的物件,都轉字串
+ (id)formatModelValue:(id)value type:(NSString *)type isEncode:(BOOL)isEncode{
    
    if (isEncode && value == nil) { // 只有物件才能為nil,基本資料型別沒值時為0
        return @"";
    }
    
    if (!isEncode && [value isKindOfClass:[NSString class]] && [value isEqualToString:@""]) {
        return [NSClassFromString(type) new];
    }
    
    if([type isEqualToString:@"i"]||[type isEqualToString:@"I"]||
       [type isEqualToString:@"s"]||[type isEqualToString:@"S"]||
       [type isEqualToString:@"q"]||[type isEqualToString:@"Q"]||
       [type isEqualToString:@"b"]||[type isEqualToString:@"B"]||
       [type isEqualToString:@"c"]||[type isEqualToString:@"C"]|
       [type isEqualToString:@"l"]||[type isEqualToString:@"L"] || [value isKindOfClass:[NSNumber class]]) {
        return value;
    }else if([type isEqualToString:@"f"]||[type isEqualToString:@"F"]||
             [type isEqualToString:@"d"]||[type isEqualToString:@"D"]){
        return value;
    }else if ([type containsString:@"Data"]) {
        return value;
    }else if ([type containsString:@"String"]) {
        if ([type containsString:@"AttributedString"]) {
            if (isEncode) {
                NSData *data = [[NSKeyedArchiver archivedDataWithRootObject:value] base64EncodedDataWithOptions:NSDataBase64Encoding64CharacterLineLength];
                return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
            }else {
                NSData* data = [[NSData alloc] initWithBase64EncodedString:value options:NSDataBase64DecodingIgnoreUnknownCharacters];
                return [NSKeyedUnarchiver unarchiveObjectWithData:data];
            }
        }
        return value;
    }else if ([type containsString:@"Dictionary"] && [type containsString:@"NS"]) {
        if (isEncode) {
            return [self stringWithDict:value];
        }else {
            return [self dictWithString:value type:type];
        }
        
    }else if ([type containsString:@"Array"] && [type containsString:@"NS"] ) {
        if (isEncode) {
            return [self stringWithArray:value];
        }else {
            return [self arrayWithString:value type:type];
        }
    }else { // 當模型處理
        if (isEncode) {  // 模型轉json字串
            NSDictionary *modelDict = [self dictWithModel:value];
            return [self stringWithDict:modelDict];
        }else {  // 字串轉模型
            NSDictionary *dict = [self dictWithString:value type:type];
            return [self model:NSClassFromString(type) Dict:dict];
        }
    }
    return @"";
}
複製程式碼

然後我們再將這個字典轉成JSON字串:

{
  "name" : "清華大學",
  "schoolId" : 1,
}
複製程式碼

思路2:直接呼叫NSJSONSerialization的方法轉成Data,然後再轉成字串就?(暫時只需要關注下面方法的if下的情況):

// 字典轉字串
+ (NSString *)stringWithDict:(NSDictionary *)dict {
    if ([NSJSONSerialization isValidJSONObject:dict]) {
        // dict -> data
        NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
        // data -> NSString
        return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    }else { // 這裡是字典巢狀物件的情況
        
        NSMutableDictionary *dictM = [NSMutableDictionary dictionary];
        for (NSString *key in dict.allKeys) {
            id value = dict[key];
            id result = [self formatModelValue:value type:NSStringFromClass([value class]) isEncode:YES];
            NSDictionary *valueDict = @{NSStringFromClass([value class]) : result};
            [dictM setValue:valueDict forKey:key];
        }
        return [[self stringWithDict:dictM] stringByAppendingString:@"CWCustomCollection"];
    }
}
複製程式碼

這樣,我們就能將School這個物件轉成字串當成值存入資料庫了。。

然後我們查詢的時候,只需要將過程反轉就OK了,首先將字串通過JSON的方法轉成字典,然後通過字典轉成對應模型,字串轉字典程式碼就不貼了,我們直接上字典轉模型的程式碼:

#pragma mark 字典轉模型
+ (id)model:(Class)cls Dict:(NSDictionary *)dict {
    id model = [cls new];
    // 獲取所有屬性名
    NSArray *ivarNames = [CWModelTool allIvarNames:cls];
    // 獲取所有屬性名和型別的字典 {ivarName : type}
    NSDictionary *nameTypeDict = [CWModelTool classIvarNameAndTypeDic:cls];
    
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        id value = obj;
        // 判斷資料庫查詢到的key 在當前模型中是否存在,存在才賦值
        if ([ivarNames containsObject:key]) {
            
            NSString *type = nameTypeDict[key];
            
            value = [CWModelTool formatModelValue:value type:type isEncode:NO];
            if (value == nil) {
                value = @(0);
            }
            [model setValue:value forKeyPath:key];
        }
    }];
    
    return model;
}
複製程式碼

這個方法的第一個入口,放在從資料庫查詢到資料對應的字典(這個在第二篇文章有說到)將該字典轉換成模型的解析函式內+ (NSArray *)parseResults:(NSArray <NSDictionary >)results withClass:(Class)cls;

然後我們對模型–>字典–>字串–>字典–>模型,這整個方法進行單獨測試:

- (void)testDictWithModel {
    
    School *school = [[School alloc] init];
    school.name = @"清華大學";
    school.schoolId = 1;
    
    Student *stu = [[Student alloc] init];
    stu.stuId = 10000;
    stu.name = @"Baidu";
    stu.age = 100;
    stu.height = 190;
    stu.weight = 140;
//    stu.dict = @{@"name" : @"chavez"};
//    stu.arrayM = [@[@"chavez",@"cw",@"ccww"] mutableCopy];
    NSAttributedString *attributedStr = [[NSAttributedString alloc] initWithString:@"attributedStr,attributedStr"];
    stu.attributedString = attributedStr;
    // 模型巢狀模型
    stu.school = school;
    
    // 模型轉字典
    NSDictionary *dict = [CWModelTool dictWithModel:stu];
    NSLog(@"-----%@",dict);
    // 字典轉字串
    NSString *jsonStr = [CWModelTool stringWithDict:dict];
    NSLog(@"=====%@",jsonStr);
    
    // 字串轉字典
    NSDictionary *dict1 = [CWModelTool dictWithString:jsonStr type:NSStringFromClass([stu class])];
    NSLog(@"-----%@",dict);
    // 字典轉模型
    id model = [CWModelTool model:[stu class] Dict:dict1];
    NSLog(@"=====%@",model);
}
複製程式碼

我們比較各個階段得到的資料,最後解析出的model和剛開始進行解析的stu資料是一一對應的,測試結果我們就不貼了(可以嘗試測試更多的場景,我這裡就沒貼程式碼了,測試一定要充足)。

2、陣列轉字串字串轉陣列

陣列轉字串分為兩種情況,一種是能直接轉JSON字串的,另一種就是陣列內的元素不是單純的基本資料型別,有可能巢狀模型,陣列,字典的情況,這時候我們要先深入把巢狀的模型,陣列,字典轉成字串再來把陣列轉JSON字串。

貼上我們陣列轉字串的程式碼:

#pragma mark 集合型別轉JSON字串
// 陣列轉字串
+ (NSString *)stringWithArray:(id)array {
    
    if ([NSJSONSerialization isValidJSONObject:array]) {
        // array -> Data
        NSData *data = [NSJSONSerialization dataWithJSONObject:array options:NSJSONWritingPrettyPrinted error:nil];
        // data -> NSString
        return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    }else {
        NSMutableArray *arrayM = [NSMutableArray array];
        for (id value in array) {
            
            id result = [self formatModelValue:value type:NSStringFromClass([value class]) isEncode:YES];
            NSDictionary *dict = @{NSStringFromClass([value class]) : result};
            [arrayM addObject:dict];
        }
        return [[self stringWithArray:arrayM] stringByAppendingString:@"CWCustomCollection"];
    }
}
複製程式碼

上面程式碼中,if下也就是第一種能直接轉的情況,第二種為不能直接轉的情況,我們需要先對每個元素進行轉換,在轉換的時候,我們把轉換之後的結果,用一個字典儲存,字典的key 為這個值所屬的類,value即為值,為什麼要這麼設計,因為我們最終都會把值變成字串存,當我們要反過來解析的時候,整個陣列都是字串,我們查詢出來的東西就無法還原到之前的型別,另一個我們在轉換成功的字串末尾追加@”CWCustomCollection”,也是因為從資料庫查詢取出的資料時,我們要分辨有些可以直接從字串轉到陣列(也就是if下第一種情況),有些並不行,我們需要按照我們的規則自己進行轉換回來。總之,這樣設計是為了之後能準確轉換回來,說到這,可能你還是一臉懵逼,其實是正常的,俗話都說實踐出真知,光看肯定不行的,最好是自己寫一個測試場景,然後思考一下如何實現,再嘗試寫一寫,而且我們這個規則也是在我們發現查詢的時候沒法實現而加上去的,所以並不是一開始就能想到要這樣做,而是打補丁打上去的

然後我們實現字串轉陣列:

字串轉陣列我們也要分為兩種情況,一種是字串的末尾帶有@”CWCustomCollection”,這種表示是我們自定義的規則轉換過來的,裡面巢狀了複雜的資料型別,另一種是不帶@”CWCustomCollection”這種我們可以直接呼叫json的方法轉回來就OK了。上程式碼:

#pragma mark JSON字串轉集合型別
// 字串轉陣列(還原)
+ (id)arrayWithString:(NSString *)str type:(NSString *)type{
    if ([str hasSuffix:@"CWCustomCollection"]) {
        NSUInteger length = @"CWCustomCollection".length;
        str = [str substringToIndex:str.length - length];
        NSJSONReadingOptions options = kNilOptions; // 是否可變
        if ([type containsString:@"Mutable"] || [type containsString:@"NSArrayM"]) {
            options = NSJSONReadingMutableContainers;
        }
        NSMutableArray *resultArr = [NSMutableArray array];
        NSData *data = [str dataUsingEncoding:NSUTF8StringEncoding];
        id result = [NSJSONSerialization JSONObjectWithData:data options:options error:nil];
        id value;
        for (NSDictionary *dict in result) {
            value = [self formatModelValue:dict.allValues.firstObject type:dict.allKeys.firstObject isEncode:NO];
            [resultArr addObject:value];
        }
        if (options == kNilOptions) {
            resultArr = [resultArr copy]; // 不可變陣列
        }
        return resultArr;
    }else {
        return [self formatJsonArrayAndJsonDict:str type:type];
    }
    
}
複製程式碼

首先,擷取掉我們自己加的字串@”CWCustomCollection”,然後我們將字串轉成對應的陣列,再遍歷陣列,分別處理解析各個元素,將得到的值新增到一個新的陣列返回。

3、字典轉字串字串轉字典

這個類似於陣列轉字串,邏輯差不多,就不貼程式碼了,因為我知道1、我廢話一大堆也不一定能表述清楚(我上面就有點表述不太好,但是我盡力了),2、想了解的一定會自己去看原始碼。。唉。。感覺嘴巴已經打結了

最終的測試結果,貼在了開頭。

本篇結束

在此,我們實現了複雜的資料型別以及字典、陣列、模型相互巢狀場景資料的儲存併合併到了插入資料的方法內,再一次成為了使用者背後默默付出的女人。下一篇文章,我們會對多執行緒安全進行處理(可能是終結篇),歡迎圍觀。

github地址
本次的程式碼,tag為1.3.0,你可以在release下找到對應的tag下載下來(注意:如果要直接執行,必須在CWDatabase.m的位置修改資料庫存放的路徑,開發除錯階段我寫在了我電腦的桌面,不修改會出現路徑錯誤,導致失敗)

最後覺得有用的同學,希望能給本文點個喜歡,給github點個star以資鼓勵,謝謝大家。

PS: 因為我也是一邊封裝,一邊寫文章。效率可能比較低,問題也會有,歡迎大家向我拋issue,有更好的思路也歡迎大家留言!

最後再為大家提供我們一步一個腳印走到今天之前文章的地址:在本文的開頭?

以及一個0耦合的仿QQ側滑框架:
一行程式碼整合超低耦合的側滑功能

啦啦啦啦。。生命不止。。推廣不斷?

相關文章