前言
在前面的三个阶段,我们分别实现的功能:
- 1、创建、打开、关闭数据库、通过runtime获取成员变量实现建对应数据库表的功能。
- 2、向数据库保存或者更新一个模型(数据)、查询数据库数据。
- 3、删除数据库数据、数据库升级(更新)、数据迁移、字段改名。
总之: 我们实现了面向模型的数据库增删查改,以及数据库升级。感觉功能实现得差不多了,但是如果存得模型得成员变量里面包含了另外得模型或者数组、字典,那么我们就没法存了。我们要解决他,这就是本篇要做的。
本篇我们要实现:复杂数据类型的存储,比如自定义对象、数组、字典等......然后我们还要实现模型嵌套模型,数组、字典嵌套模型以及各种相互嵌套的情况。 本篇思路有点绕,需要沉着冷静并且实践才行。先看一下我们最终实现的结果,我们向数据库内存储一个非常复杂的模型:
插入数据库是成功的,但是插入成功不重要,重要的是,你取出来的时候,他是不是插入之前的样子,下面我们进行数据库查询,得到以下结果: 从得到的结果来看,各种类型嵌套的模型,我们能过完美的插入数据库,同时,我们也能完美将它从数据库取出来并且还原为模型,灰常的牛逼。这个一定是我们比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、想了解的一定会自己去看源码。。唉。。感觉嘴巴已经打结了
最终的测试结果,贴在了开头。
本篇结束
在此,我们实现了复杂的数据类型以及字典、数组、模型相互嵌套场景数据的存储并合并到了插入数据的方法内,再一次成为了用户背后默默付出的女人。下一篇文章,我们会对多线程安全进行处理(可能是终结篇),欢迎围观。
本次的代码,tag为1.3.0,你可以在release下找到对应的tag下载下来(注意:如果要直接运行,必须在CWDatabase.m的位置修改数据库存放的路径,开发调试阶段我写在了我电脑的桌面,不修改会出现路径错误,导致失败)
最后觉得有用的同学,希望能给本文点个喜欢,给github点个star以资鼓励,谢谢大家。
PS: 因为我也是一边封装,一边写文章。效率可能比较低,问题也会有,欢迎大家向我抛issue,有更好的思路也欢迎大家留言!
最后再为大家提供我们一步一个脚印走到今天之前文章的地址:在本文的开头?
以及一个0耦合的仿QQ侧滑框架:
啦啦啦啦。。生命不止。。推广不断?