前言
在前面的三個階段,我們分別實現的功能:
- 1、建立、開啟、關閉資料庫、通過runtime獲取成員變數實現建對應資料庫表的功能。從0開始弄一個面向OC資料庫(一)
- 2、向資料庫儲存或者更新一個模型(資料)、查詢資料庫資料。從0開始弄一個面向OC資料庫(二)
- 3、刪除資料庫資料、資料庫升級(更新)、資料遷移、欄位改名。從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側滑框架:
一行程式碼整合超低耦合的側滑功能
啦啦啦啦。。生命不止。。推廣不斷?