从简书迁移到掘金git
本文并非CoreData从入门到精通之类的教程, 并不会涉及到过多的原理概念描述, 而是介绍如何让CoreData的使用变得更加简单明了, 方便亲民. 全文约六千字, 预计花费阅读时间15分钟.github
大概是去年夏天, 由于要作数据缓存开始使用MagicalRecord. 在和同事们使用了一段时间后, 发如今复杂的业务场景中单纯的MagicalRecord应用起来仍是略有些麻烦, 因而就在它的基础上再封装了一层, 最后实现的结果仍是比较满意的. 由于比较实用, 并且原理很简单, 索性就开篇博客把这个工具放出来.数据库
这里以一段和同事A(A没有任何CoreData方面的开发经验, 因此比较有表明性)的对话描述一下这个工具如何使用: 我: 东西写完了, A啊, 你过来看看, 我给你说说怎么用. 假设你如今要存一个Snack类, 类定义呢大概像下面这个样子:数组
@interface Snack : NSObject
@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *taste;
@property (assign, nonatomic) float size;
@property (assign, nonatomic) float price;
@property (assign, nonatomic) NSInteger snackId;
@end
复制代码
你须要作的就是在.xcdatamodeld里面添加一个Entity, 随便取个名字, 好比CoreSnack吧, CoreSnack里面的字段就是你要存的那些属性名, 注意: 名字和类型尽可能一一对应.缓存
如今你有了一个本地Model类Snack和一个NSManagedObject类CoreSnack, 他们两的关系就至关于本地Model和网络数据的Protobuf/JSon, 只不过此次他们的关系是双向的, 咱们不只能够将CoreSnack转换成Snack, 也能够将Snack转换成CoreSnack.安全
A: 额, JSon/Protobuf/ManagedObject转Model很简单, 直接用以前写的转换工具就好了, Model怎么转ManagedObject? Model又不知道本身对应的ManagedObject类是哪个? 还有, 难道每次转换都存一个新数据进去? 那不是好多重复数据?bash
我: 嗯, 你说的很对, 因此你须要在Snack里面声明它对应的ManagedObject是哪一个, 还有这个ManagedObject的主键, 默认状况下, 我会用主键去重. 像这样:网络
@implementation Snack
#pragma mark - CoreData
//Model和CoreData对应关系
+ (Class)managedObjectClass {
return NSClassFromString(@"CoreSnack");
}
//主键 (key是Model属性名, value是CoreData字段名, 通常状况下是同样的, 声明成字典只是以防万一)
+ (NSDictionary *)primaryKeys {
return @{@"snackId" : @"snackId"};
}
@end
复制代码
A: 奥, 行吧. 那这些东西我都声明好了的话, 我怎么存东西, 要本身调用CoreData的接口吗?多线程
我: 不须要你调用CoreData接口, 你要作的事情很简单: 新建, 赋值, 存储. 像这样:并发
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //赋值
snack.size = ...; //赋值
snack.name = ...; //赋值
//... 各类赋值
[snack save]; //存储
复制代码
A: 看着还蛮简单的, 可是万一我要存的东西比较多, 这样会不会卡UI?
我: 虽然不知道为何一个Model会存不少东西, 可是我也提供了接口, 像这样:
Snack *snack = [Snack new]; //新建
//... 各类赋值
[snack saveWithCompletionHandler:^{
}]; //异步存储
复制代码
A: 那我要存一个Snack数组的话, 怎么搞? forin吗?
我: 不行, 每次存储都是要访问数据库的, 用forin的话会屡次访问数据库, 很耗时的! 若是你要存一个Model数组, 用下面这个接口:
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 1; i < 9; i++) {
Snack *snack = [Snack instanceWithId:i];
[snacks addObject:snack];
}
[Snack saveObjects:snacks]; //异步存储无回调接口(数组存储只提供异步接口)
[Snack saveObjects:snacks completionHandler:^{
}]; //异步存储有回调接口
复制代码
A: 你这个好像只能存普通数据类型, 那若是个人Model有几个属性自己也是Model, 有的属性也有对应的ManagedObject, 有的没有, 有的甚至是数组, 怎么办?
我: 没有关系, 也是同样的用法, 你只管设置, 接口会帮你存好的, 可是若是你的属性里面有映射到ManagedObject的Model数组的话, 你最好用异步存储的接口:
Ticket *ticket = [Ticket instanceWithId:0];
Worker *worker = [Worker instanceWithId:0];
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 10; i < 19; i++) {
[snacks addObject:[Snack instanceWithId:i]];
}
worker.snacks = snacks; //CoreSnack
worker.ticket = ticket; //CoreTicket
[worker save]; //同步存储
[worker saveWithCompletionHandler:nil];//异步存储
复制代码
A: 嗯... 东西存进去了, 那怎么取出来?
我: 由于存东西是分单个存储和数组存储, 因此取东西也给了两组接口, 我一组一组给你演示. 先是单个查询的接口:
[Snack findFirstByAttribute:@"snackId" withValue:@0]; //单个同步查询接口1
[Snack findFirstWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 0"]]; //单个同步查询接口2
[Snack findFirstWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 0"] sortedBy:@"price" ascending:YES]; //单个同步查询接口3
//异步查询接口直接在后面加上 completionHandler 参数便可
复制代码
数组查询接口:
// 查询接口大致分两种: 条件查询和分页查询(这里列的都是同步查询接口)
// 条件查询
[Snack findAll];
[Snack findAllWithPredicate:predicate];
[Snack findAllSortedBy:someSortItem ascending:isAscend];
[Snack findAllSortedBy:someSortItem ascending:isAscend withPredicate:predicate];
[Snack findByAttribute:someAttribute withValue:aValue];
[Snack findByAttribute:someAttribute withValue:aValue andOrderBy:someSortItem ascending:isAscend];
// 分页+条件查询 page起点为0 row最大值为1000
[Snack findAllWithPage:page row:row];
[Snack findAllWithPredicate:somePredicate page:page row:row];
[Snack findAllSortedBy:someSortItem ascending:isAscend page:page row:row];
[Snack findAllSortedBy:someSortItem ascending:isAscend withPredicate:somePredicate page:page row:row];
[Snack findByAttribute:someAttribute withValue:aValue page:page row:row];
// 同理, 异步查询接口直接在后面加上 completionHandler 参数便可
复制代码
我: 另外, 若是从CoreData取数据的时候, 某个属性也是一个Model数组, 记得在这条数据对应的Model中里面声明属性数组里面是什么Model, 这点和JSon/Protobuf是同样的, 好比上面的Worker有个snacks数组属性, 数组元素也是Model, 你就要在Worker里面这样声明一下:
@implementation Worker
+ (Class)managedObjectClass {
return NSClassFromString(@"CoreWorker");
}
+ (NSDictionary *)containerPropertyKeypathsForCoreData {
return @{@"snacks" : @"Snack"};
}
@end
复制代码
A: 行啦, 我知道了. 如今有存有取, 那改数据怎么改, 是否是要先查询, 而后改数据, 改完了再存进去?
我: 嗯, 逻辑是这个逻辑, 可是不用你写这些代码, 你只须要提供查询条件就好了, 像这样:
//xxxClass - saveSanck
- (void)saveSanck {
//在某个位置事先有存过一条CoreSnack记录
Snack *snack = [Snack new]; //新建
snack.snackId = 123;//
snack.price = 1.1;
snack.name = @"name1";
//... 各类赋值
[snack save]; //存储
}
//yyyClass - modifSanck
- (void)modifSanck {
//当你须要改这条数据的时候
// 法1
Snack *snack = [Snack new]; //新建
snack.name = @"xxx"; //要改的部分直接赋值
snack.size = 999; //要改的部分直接赋值
[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
//内部会以你提供的Predicate进行查询 你设置的值进行更改 最后进行更新存储
// 法2
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //设置要改的那条记录对应的主键值
snack.name = @"xxx"; //要改的部分直接赋值
snack.size = 999; //要改的部分直接赋值
[snack save];
//若是不写查询条件 默认会以主键为做为查询条件
//等同于[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
// 法3 和 法2 相似 不过此次会以提供的查询数组生成查询条件
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //设置要改的那条记录对应对应查询值
snack.otherProperty = yyy;//设置要改的那条记录对应查询值
snack.name = @"xxx"; //要改的部分直接赋值
snack.size = 999; //要改的部分直接赋值
[snack saveWithEqualProperties:@[@"snackId", @"otherPrimaryKey"]];
//若是设置了查询条件数组 会以你提供的查询数组生成查询条件
//等同于[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123 && otherProperty = yyy"]];
//note: 在数据修改时, 直接给某个值设置为nil或者0对应的CoreData在数据库中不会有任何改变 好比snack.snackId = 0和snack.name = nil都是无效的
//若是你确实想将某个字段设置为空, 那就设置对应的空值, 好比snack.name = @""和snack.snackId = CDZero(由于数字没有空值, 因此我声明了一个保留字)
//同理 以上的同步操做接口加上 completionHandler 就是对应的异步操做接口
}
复制代码
A: 有个问题, 这个接口看起来和存储接口好像啊. 若是我给的查询条件有问题, 最后没有查到数据库里面的数据, 或者数据库里面根本就没有这条数据, 会怎么样?
我: 查询查不到的话, 那就直接存一条数据进去咯, 数据存储其实调用的就是数据更新接口, 否则你觉得我怎么去重的?
A: 额, 我担忧我写错查询条件, 能不能查不到就不存?
我: 能够啊, 加个参数或者把save和update分开就好了, 不过我懒, 没实现, 你须要就本身实现咯!
接下来是数组的批量更新接口:
//将你想要更新的批量数据 放到一个数组里面 而后经过经过HHPredicate(注意: 不是NSPredicate)设置查询条件
//HHPredicate定义了查询条件的 ==(equalKeys) 关系和 in(containKeys) 关系 批量更新会根据这些关系去进行数据查询
//HHPredicte的关系键值对和Model的主键键值对同样 key是Model的属性名, value是CoreData的字段名
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 1; i < 9; i++) {
Snack *snack = [Snack new]; //新建
snack.snackId = i;
snack.name = ...; //要改的部分直接赋值
snack.size = ...; //要改的部分直接赋值
[snacks addObject:snack];
}
//法 1
[Snack saveObjects:snacks checkByPredicate:[HHPredicate predicateWithEqualKeys:nil containKeys:@{@"snackId" : @"snackId"}]];
//内部会生成一个NSPredicate = [NSPredicate predicateWithFormat:@"snackId in {1, 2, 3, 4...}"]
//这里的例子没有==关系, 若是是复合键作主键的话, 会频繁用到equalKeys, 或者想优化查询速度也能够设置equalKeys
//好比设置了equalKeys{@"xxxProperty" : @"xxx"}就会生成[NSPredicate predicateWithFormat:@"xxxProperty = xxx && snackId in {1, 2, 3, 4...}"]
//法 2 由于通常设置Model的属性名和CoreData的字段名都是同样的 因此直接给个便利的方法出来
[Snack saveObjects:snacks checkByPredicate:[HHPredicate predicateWithEqualProperties:nil containProperties:@[@"snackId"]]];
复制代码
批量更新和单个更新同样, 查询条件查不到的那部分数据就会被认为是普通存储, 会新建这部分的数据并存储到数据库中.
A: 只剩下删除了, 这部分是什么样子的?
我: 删除和更新接口差很少, 不过要简单多了, 像这样:
// 法1
Snack *snack = [Snack new]; //新建
[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]]; //内部会以你提供的Predicate进行查询 而后删除
// 法2
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //设置要删除的那条记录对应的主键值
[snack delete]; //若是不写查询条件 默认会以主键为做为删除条件
// 等同于[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
// 法3
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //设置要删除那条记录的知足条件1
snack.otherPrimaryKey = xxx;//设置要删除那条记录的知足条件2
[snack deleteWithEqualProperties:@[@"snackId", @"otherPrimaryKey"]];
// 等同于[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"otherPrimaryKey = xxx && snackId = 123"]];
复制代码
批量删除更简单:
[Snack deleteAll]; //所有删除
[Snack deleteAllMatchingPredicate:[NSPredicate predicateWithFormat:@"snackId <= 10"]]; //删除知足条件的部分
复制代码
A: 增删改查算是齐了, 可是我据说CoreData是线程不安全的, 那我在使用的时候须要注意什么? 还有多线程的数据同步呢?
我: 多线程和数据同步的问题不须要你关心, 你只管记住上面的这些接口就好了, 好比下面的写法是彻底没问题的:
- (void)makeSnackOnOtherThread {
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 100; i < 109; i++) {
[snacks addObject:[Snack instanceWithId:i]];
}
[Snack saveObjects:snacks];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//查询操做会等到最近的一次存储/删除/更新操做完成后才执行(即数据同步)
NSArray *snacks = [Snack findAllSortedBy:@"snackId" ascending:YES withPredicate:[NSPredicate predicateWithFormat:@"snackId >= 100 && snackId < 109"]];
//子线程查询的数据拿到主线程去用也彻底没问题(即安全的跨线程访问)
dispatch_async(dispatch_get_main_queue(), ^{
[snacks enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj log];
}];
});
});
}
- (void)makeSnackOnOtherThread2 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子线程新建
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 110; i < 119; i++) {
[snacks addObject:[Snack instanceWithId:i]];
}
dispatch_async(dispatch_get_main_queue(), ^{
//可是拿到主线程存储
[Snack saveObjects:snacks];
dispatch_async(dispatch_get_global_queue(2, 0), ^{
//而后又到另外一个子线程查询
NSArray *snacks = [Snack findAllSortedBy:@"snackId" ascending:YES withPredicate:[NSPredicate predicateWithFormat:@"snackId >= 110 && snackId < 119"]];
[snacks enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
[obj log];
}];
});
});
});
}
复制代码
A: 那敢情好, 因此如今我要操做CoreData数据只须要作三件事:
1.根据业务需求新建本地Model同时在.xcdatamodeld里面新建与Model对应的Entity, 并设置相应字段.
2.在本地Model内部声明和对应的NSManagedObject类的映射关系.
3.调用相应接口进行增删改查.
在说实现原理以前, 先看看现有的CoreData用起来有哪些缺点, 或者说麻烦的地方. 咱们都知道, 在对CoreData作任何操做时, 都是经过一个Context来完成的, Context内部管理着和本身相关的ManagedObject, 对外则提供各类操做这些对象的相应接口, 它的本质实际上是处在应用和数据库之间的一层全局缓存(在Context和实际的数据库之间还有一层NSPersistentStoreCoordinator, 但这和本文要讲的东西联系不大, 略去不谈).
缓存带来的效益是可观的, 在缓存命中的状况下, 在内存中操做数据确定比直接访问数据库效率要高, 这在数据读写频繁或者数据量大时尤其有效. 但事有利弊, 缓存带来了效率的提高同时也引出了一些问题. 比较突出的有两点:总是被人诟病的内存问题以及因缓存数据同步而引起的实际使用时操做繁琐复杂.
第一点几乎是无解的(但iPhone的配置一直在提高, 因此这个问题如今也不算什么问题了), 咱们主要聊聊第二点. 共享数据在多线程操做的状况下势必是要作数据同步的(否则我在这个线程用你在那个线程改, 你在这个线程改他在那个线程删, 数据不是全乱套了), Context做为一个全局缓存天然也不例外. 系统对此采起的措施是: 为Context配置相应的并发操做类型(NSManagedObjectContextConcurrencyType). 这个并发操做类型定义了Context的数据操做规范: 从Context读取的数据只能在当前获取数据的线程中访问, 而更改数据的操做只能在Context对应的操做队列中执行(MainConcurrencyType的操做队列就是UI线程, 而PrivateConcurrencyType的操做队列由系统自建, 对外不可见, 外部只能经过performBlock将操做添加到这个队列执行). 这套操做规范解决了缓存内部的数据同步问题, 但却间接引发了更加麻烦的问题: 缓存间的数据同步.
咱们来看看缓存内数据同步是如何引出缓存间数据同步问题的:
1.数据更改操做只能在Context对应的操做队列执行, 若是此时的Context对应的操做队列是主线程, 耗时的数据操做就会卡UI, 这是不能接受的.
2.耗时操做不要在UI线程作, 因而一般咱们会至少创建一个子线程的Context来作耗时操做.
3.多个Context其实也就是多个缓存块, 它们之间是各自独立毫无联系的, 这意味着在一个Context进行的任何操做另外一个Context是不知情的, 因此你不能期望在子线程存储后再去UI线程读取, 没有用. 即便之后容许这样作, 你也必须等到子线程数据更新完成后才能到主线程进行查询, 不然就是数据错乱. 另外, 由于从Context读取的数据只能在当前获取数据的线程中访问, 因此也不能从子线程读取数据后直接传递到UI线程使用, 没有用...
4.按下葫芦浮起瓢, 为了解决缓存内数据同步, 咱们不得不处理缓存间数据同步, 并且, 这个过程系统能提供的帮助十分有限...(缓存间数据同步目前业界有许多方案, 诸如Context操做完成后发通知让其余Context进行数据同步, 两层/三层基于child/parentContext的设计等等, 另外还涉及到数据传递和数据冲突解决, 内容较多且与本文无关, 这里不作细表)
哎, 光是各类Coordinator/Context/Objcet/ConcurrencyType对象之间的关系就够麻烦的了, 如今还要本身搞数据同步, 这让不少刚接触CoreData的同窗热情大减, 纷纷表示CoreData太复杂了, 仍是用SQLite/Realm吧...
好了, 上面长篇大论一顿分析, 如今终于讲到本文的目的了: 如何实现一套支持线程安全且简单易用的CoreData工具?
回答这个问题咱们须要把视线挪到分析缓存间数据同步的第一点, 而后想想: 若是一开始Context就支持在子线程操做数据同时也能在UI线程访问数据并且还自带数据同步特效, 后面的一系列问题不就都没有了吗?
把这个想法当成一个需求, 咱们来作一个简单的可行性分析. 这个需求一共三点:子线程操做数据, UI线程访问数据, 多线程数据同步.
第一点很简单, 直接设置Context并发操做类型为PrivateConcurrencyType就好了, 第三点也很简单, 多线程数据同步就是加锁嘛, 读写频繁的话把锁换成dispatch_barrier_async/sync和dispatch_async/sync的组合就好了.
第二点就麻烦一点了, 由于NSManagedObject是和Context强关联的, 想要脱离Context的线程限制进行数据访问是不太现实的. 对此, 咱们须要绕一个小弯, 即在可访问的线程中将NSManagedObject的值映射到一个能够跨线程访问的对象上(也就是咱们的Model), 在待使用线程使用这个映射对象而不是NSManagedObject, 借此解决跨线程访问的问题. 最后, 当咱们在须要对数据进行任何修改时, 先将映射对象还原相应的NSManagedObject, 再经过Context去到子线程执行对应的操做. 饶了一个弯, 不过还好, 由于最麻烦的互相转换的工具好久之前就已经实现了, 直接把以前写的Protobuf解析器简单改改就能够用了.
原理大概就是这样了, 归纳起来其实咱们只作了两件事情:
进一步的, 这套路其实算是ORM的变种实现(CoreData自己其实就是ORM的一种实现, 默认映射关系是SQLite--NSManagedObject). 理论上, 咱们只要换一个数据转换工具, 重写一下数据操做接口, 那么下层即便换掉CoreData改用SQLite/Realm/xxx也是同样的.
上面说到的原理很简单, 具体实现起来也很简单, 这里我就简单贴贴代码, 主要说一下细节问题就好.
//某个分页同步查询接口
+ (NSArray *)findAllWithPage:(NSUInteger)page row:(NSUInteger)row {
return [self objectsWithManagedObjectsFetchHandler:^NSArray *(id managedObjectClass) {
return [self managedObjectsWithFetchRequest:[managedObjectClass MR_requestAllInContext:self.saveContext] page:page row:row];
}];
}
...若干同步查询接口
//某个不分页异步查询接口
+ (void)findAllWithCompletionHandler:(void (^)(NSArray *objects))completionHandler {
[self converObjectsWithManagedObjectsFetchHandler:^NSArray *(id managedObjectClass) {
return [managedObjectClass MR_findAllInContext:self.saveContext];
} completionHandler:completionHandler];
}
...若干异步查询接口
复制代码
查询的实现很简单, 经过Model声明的映射关系拿到NSManagedObject类, 而后执行查询, 将查询结果转换成Model传递出来便可, 由于这部分逻辑都是同样的, 因此就直接写出一系列通用方法, 各个查询接口调用这些通用方法便可. 具体的方法实现以下:
//某个同步查询通用方法
+ (NSArray *)objectsWithManagedObjectsFetchHandler:(NSArray *(^)(id managedObjectClass))fetchHandler {
IfInvalidManagedObjectClassReturn(nil);
__block NSArray *objects;
dispatch_sync(self.perfromQueue, ^{
objects = [self objectsWithManagedObjects:fetchHandler(managedObjectClass)];
});
return objects;
}
//某个异步查询通用方法
+ (void)converObjectsWithManagedObjectsFetchHandler:(NSArray *(^)(id managedObjectClass))fetchHandler completionHandler:(void (^)(NSArray *objects))completionHandler {
IfInvalidManagedObjectClassBreak;
dispatch_async(self.perfromQueue, ^{
NSArray *objects = [self objectsWithManagedObjects:fetchHandler(managedObjectClass)];
dispatch_async(dispatch_get_main_queue(), ^{
completionHandler ? completionHandler(objects) : nil;
});
});
}
复制代码
全部的查询方法最后都会走到一个解析方法去作数据转换, 该方法以下:
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject {
if (managedObject == nil) { return nil; }
id object = [self new];
HHClassInfo *classInfo = [NSObject managedObjectClassInfoWithObject:object];
NSDictionary *containerPropertyKeypaths = [(id)classInfo.cls respondsToSelector:@selector(containerPropertyKeypathsForCoreData)] ? [classInfo.cls containerPropertyKeypathsForCoreData] : nil;
for (HHPropertyInfo *property in classInfo.properties) {
if ([(id)managedObject respondsToSelector:property->_getter]) {
id propertyValue = [managedObject valueForKey:property->_getPath];
if (propertyValue != nil) {
switch (property->_type) {
case HHPropertyTypeBool:
case HHPropertyTypeInt8:
case HHPropertyTypeUInt8:
case HHPropertyTypeInt16:
case HHPropertyTypeUInt16:
case HHPropertyTypeInt32:
case HHPropertyTypeUInt32: {
if ([propertyValue respondsToSelector:@selector(intValue)]) {
((void (*)(id, SEL, int))(void *) objc_msgSend)(object, property->_setter, [propertyValue intValue]);
}
} break;
//...各类格式的数据赋值
case HHPropertyTypeCustomObject: {
propertyValue = [property->_cls objectWithManagedObject:propertyValue];
if (propertyValue) {
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, propertyValue);
}
} break;
case HHPropertyTypeArray: {
if ([propertyValue isKindOfClass:[NSString class]]) {
if ([propertyValue length] > 0) {
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, [propertyValue componentsSeparatedByString:@","]);
}
} else {
id objectsClass = NSClassFromString(containerPropertyKeypaths[property->_name]);
if (!objectsClass) { break; }
NSMutableArray *objects = [NSMutableArray array];
for (id managedObj in propertyValue) {
id value = [objectsClass objectWithManagedObject:managedObj];
if (value) { [objects addObject:value]; }
}
if (objects.count > 0) {
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, objects);
}
}
} break;
//...各类格式的数据赋值
}
}
}
}
return object;
}
复制代码
这块的逻辑和Protobuf解析差很少(Protobuf解析的具体逻辑), 无非就是各类数据格式的赋值, 自定义类和数组属性的特殊处理. 不过和以前的解析不一样, 在数组这块多了一个字符串判断, 而后才是数组属性的解析, 简单解释下:
CoreData默认是不能存数组的, 要存数组须要走transformable而后本身手写序列化, 这个过程其实也不过是一次中间值转化, 存储时将数组转化为Data, 获取时将Data转化回数组. 但咱们这套工具的目的是使用简单, 因此不想让使用者关心这些东西, 默认我会在存储时将数组经过逗号分割拼装成字符串放进数据库, 而后在获取时将字符串再解析回数组. 另外, transformable的存储方式是不支持条件查询的, 由于它是存储的是Data, 无从比对, 但分割字符串的方式显然是支持条件查询的, 并且还不用使用者写任何多余代码. (固然, transformable还有其余的应用场景, 好比存UIImage之类的对象, 这种状况仍是须要本身手写序列化的, 但这种状况不属于数组, 属于FoundationObject)
//经过默认的主键去重进行存储或者更新
- (void)save {
IfUndefinedPrimaryKeyBreak;
[self saveWithPredicate:[[HHPredicate predicateWithEqualKeys:[objectClass primaryKeys]] makePredicateWithObjcet:self]];
}
- (void)saveWithEqualProperties:(NSArray *)properties {
[self saveWithPredicate:[HHPredicate makePredicateWithObjcet:self equalProperties:properties]];
}
//经过给定的条件去重进行存储或者更新
- (void)saveWithPredicate:(NSPredicate *)predicate {
IfInvalidManagedObjectClassBreak;
dispatch_barrier_sync(NSObject.perfromQueue, ^{
//经过给定的条件进行查询 有就更新 没有就新建
NSManagedObject *managedObject = [NSObject managedObjectWithClass:managedObjectClass predicate:predicate];
//经过Model配置NSManagedObject而后存储
[managedObject saveWithObject:self];
});
}
//经过给定的条件进行查询 有就更新 没有就新建
+ (NSManagedObject *)managedObjectWithClass:(id)managedObjectClass predicate:(NSPredicate *)predicate {
if (predicate == nil || managedObjectClass == nil || ![managedObjectClass isSubclassOfClass:[NSManagedObject class]]) { return nil; }
NSManagedObject *managedObject = [managedObjectClass MR_findFirstWithPredicate:predicate inContext:NSObject.saveContext];
if (managedObject != nil) {
return managedObject;
} else {
return [managedObjectClass MR_createEntityInContext:NSObject.saveContext];
}
}
复制代码
为了方便, 添加数据和修改数据走的是一个接口, 同时添加和更新还分为单个操做和批量操做, 这里先介绍单个操做. 代码中的注释应该很明显了, 咱们直接跳到配置并存储数据的部分:
- (void)saveWithObject:(id)object {
if (!object || [object isKindOfClass:[self class]]) { return ; }
[self configWithObject:object];
[NSObject.saveContext MR_saveToPersistentStoreAndWait];
}
- (void)configWithObject:(id)object {
if (!object || [object isKindOfClass:[self class]]) { return ; }
HHClassInfo *classInfo = [NSObject managedObjectClassInfoWithObject:object];
NSDictionary *containerPropertyKeypaths = [(id)classInfo.cls respondsToSelector:@selector(containerPropertyKeypathsForCoreData)] ? [classInfo.cls containerPropertyKeypathsForCoreData] : nil;
for (HHPropertyInfo *property in classInfo.properties) {
if ([self respondsToSelector:property->_getter]) {
id propertyValue = [object valueForKey:property->_name];
if (propertyValue != nil) {
if (property->_type >= HHPropertyTypeBool && property->_type < HHPropertyTypeArray) {
float number = [propertyValue floatValue];
if (number == 0) { continue; }
if (number == CDZero) { propertyValue = @0; }
} else if (property->_type == HHPropertyTypeCustomObject) {
if (![(id)property->_cls respondsToSelector:@selector(primaryKeys)]) { continue; }
NSPredicate *predicate = [[HHPredicate predicateWithEqualKeys:[property->_cls primaryKeys]] makePredicateWithObjcet:propertyValue];
NSManagedObject *managedObject = [NSObject managedObjectWithClass:[property->_cls matchedManagedObjectClass] predicate:predicate];
[managedObject configWithObject:propertyValue];
propertyValue = managedObject;
} else if (property->_type == HHPropertyTypeArray) {
if ([propertyValue count] == 0) {
[self setValue:nil forKeyPath:property->_getPath];
continue;
}
//数组且数组内只是普通数据类型
id element = [propertyValue firstObject];
if ([element isKindOfClass:[NSString class]] ||
[element isKindOfClass:[NSNumber class]]) {
propertyValue = [propertyValue componentsJoinedByString:@","];
} else {//数组且数组内也是NSManagedObject
//...这部分和批量操做差很少 下文介绍
}
}
if (propertyValue) { [self setValue:propertyValue forKeyPath:property->_getPath]; }
}
}
}
}
复制代码
经过Model配置NSManagedObject也就是Model解析的逆操做, 而NSManagedObject自己只支持KVC的方式进行赋值, 因此比起Model解析部分的各类MsgSend和数据格式判断要简单的多, 这里我只介绍一点: 数据初始化和数据合并.
当咱们在对CoreData作修改操做时其实就是一次数据合并操做, 咱们将此时须要修改的值覆盖数据库原有的值, 可是不须要修改的部分是不变的, 这能够看作是两个Model各自将一部分数据进行组装生成第三个合并Model.
在这套工具中, 第一个Model就是数据库中原有的值, 第二个Model就是咱们想要修改数据的的值, 合并的逻辑是将第二个Model中不为空的部分(也就是咱们设置修改的部分)赋值给第一个Model, 而后将更新后的Model存回数据库. 但这有一个问题, 那就是若是我就是想将数据库中的值置空怎么办?
目前个人处理是, 若是你确实想将某个值置空, 那就传对应的空值而不是nil, 由于直接设置nil是不作覆盖的. 好比, 你想将某个字符串属性置空, 那就传@"", 数组就传@[], 若是你想将某个数字置0, 那就传CDZero(这是我声明的一个保留字, 由于数字在KVC获取时是不可能为空的, 拿到的都是0), 这些空值在被覆盖后从新获取时会被断定为nil, 以达到置空的目的. 当咱们向CoreData增长数据时其实作的是数据初始化, 但由于数据初始化是数据合并的子集, 因此数据初始化就直接用数据合并的逻辑了.
单个数据添加和修改说完了, 接下来看看批量添加和修改:
//批量添加/更新便利方法1
+ (void)saveObjects:(NSArray *)objects {
[self saveObjects:objects completionHandler:nil];
}
//批量添加/更新便利方法2
+ (void)saveObjects:(NSArray *)objects completionHandler:(void (^)())completionHandler {
HHPredicate *predicate;
if (objects.count > 0) {
id objectClass = [objects.firstObject class];
if ([objectClass respondsToSelector:@selector(primaryKeys)]) {
predicate = [HHPredicate predicateWithContainKeys:[objectClass primaryKeys]];
}
}
[self saveObjects:objects checkByPredicate:predicate completionHandler:completionHandler];
}
//批量添加/更新便利方法3
+ (void)saveObjects:(NSArray *)objects checkByPredicate:(HHPredicate *)predicate {
[self saveObjects:objects checkByPredicate:predicate completionHandler:nil];
}
//实际执行批量添加/更新的方法
+ (void)saveObjects:(NSArray *)objects checkByPredicate:(HHPredicate *)predicate completionHandler:(void (^)())completionHandler {
id managedObjectClass = [self matchedManagedObjectClass];
if (objects.count == 0 || managedObjectClass == nil || predicate == nil) {
DispatchCompletionHandlerOnMainQueue;
} else {
dispatch_barrier_async(NSObject.perfromQueue, ^{
//1. 根据查询条件的查询数据中已有的部分
NSArray *managedObjects = [managedObjectClass MR_findAllWithPredicate:[predicate makePredicateWithObjcets:objects] inContext:self.saveContext];
//2. 以 HHPredicate 中的惟一标识符规则从NSManagedObject处生成一个的标识符X数组
NSMutableArray *managedObjectIdentifierArray = [NSMutableArray array];
for (NSManagedObject *managedObject in managedObjects) {
id managedObjectIdentifier = [predicate identifierWithManagedObjcet:managedObject];
if (managedObjectIdentifier) {
[managedObjectIdentifierArray addObject:managedObjectIdentifier];
}
}
//3. 遍历须要更新/存储的Model数组
for (id object in objects) {
//3.1 以 HHPredicate 中的惟一标识符规则从Model处也生成一个标识符Y
NSManagedObject *managedObject;
id objectIdentifier = [predicate identifierWithObjcet:object];
//3.2 若是标识符Y和步骤2中生成的标识符X匹配, 说明它是已经存在于数据库中, 即修改操做
if ([managedObjectIdentifierArray containsObject:objectIdentifier]) {
managedObject = [managedObjects objectAtIndex:[managedObjectIdentifierArray indexOfObject:objectIdentifier]];
} else {//3.3 标识符Y和标识符X不匹配, 说明是添加操做
managedObject = [managedObjectClass MR_createEntityInContext:self.saveContext];
}
//3.4 根据Model配置managedObject(新建的或者数据库原本就有的)
[managedObject configWithObject:object];
}
//4. 将添加/修改提交到数据库
[self.saveContext MR_saveToPersistentStoreAndWait];
DispatchCompletionHandlerOnMainQueue;
});
}
}
复制代码
- (NSString *)identifierWithObjcet:(id)object {
return [self identifierWithKeys:self.containKeys.allKeys objcet:object];
}
- (NSString *)identifierWithManagedObjcet:(id)managedObject {
return [self identifierWithKeys:self.containKeys.allValues objcet:managedObject];
}
- (NSString *)identifierWithKeys:(NSArray *)keys objcet:(id)object {
if (keys.count > 0) {
if (keys.count > 1) {
keys = [keys sortedArrayUsingComparator:^NSComparisonResult(NSString * _Nonnull obj1, NSString * _Nonnull obj2) {
return [obj1 compare:obj2];
}];
}
NSMutableString *identifier = [NSMutableString string];
[keys enumerateObjectsUsingBlock:^(id _Nonnull key, NSUInteger idx, BOOL * _Nonnull stop) {
[identifier appendFormat:@"%@:", [object valueForKey:key]];
}];
return [identifier copy];
}
return nil;
}
复制代码
能够看见, 批量操做比单个操做要复杂一些, 由于批量操做中经常同时存在添加和更新. 举个例子: 数据库中第一天存了一些用户好友, 次日可能这些好友有些改了昵称/头像/个性签名而后用户本身在网页端又新添加了一些好友, 此时咱们直接调用接口拉取下来的第一页几十条数据中就确定有部分是修改, 有部分是添加的, 若是让使用者本身查询而后区分哪部分是添加, 哪部分是修改, 无疑增长了使用复杂度, 因此这些东西我也选择由工具本身来作而不是抛给使用者. 这也是为何从一开始, 数据更新和数据存储就是走的一个接口的缘由, 由于批量操做遇到这种状况简直不要太多.
咱们经过HHPredicate来生成去重的查询条件, 同时还生成Model和NSManagedObject的惟一标识符, 那么这个惟一标识符是怎么生成的呢? 其实很简单, HHPredicate定义了equal(==)和contain(in)关系, equal中的字段定义了整个待操做的数组Model值相同的字段, 一般这部分用做缩小查询范围加快查询速度, 是无关紧要的. contain定义了数组中每一个Model都不相同的字段, 是必需要有的, 一般这个字段就是主键. 举个例子: 好比咱们要存一个User数组, 这个数组中每一个User的主键UserId确定都是不一样的, 固然, 其余的字段诸如年龄, 名字可能也不一样, 可是咱们只须要一个字段就足够标识了, 因此此时contain定义就填UserId. 那equal定义呢? 你能够不填, 可是若是这些User确实有一个字段全都同样, 好比全都是xxx公司的员工, 那你能够在equal定义填上xxx公司, 这样的查询会比较快.
equal定义在复合键作主键时特别有用, 由于不少时候批量操做只有一部分是彻底不一样的, 另外一部分都是同样的. 好比三年二班的学生, 他们的学号一般是不一样的1到100, 可是班级都是三年二班. 单凭学号不足以惟一标识一个学生, 毕竟其余班也有1到100的学号, 可是加上班级后就能够了. 在实际使用中, 一般一个手机能够有若干个帐号, 每一个帐号都有若干好友/做品..., 单凭好友/做品Id是不足以作惟一标识符的, 还必须加上当前登陆的用户Id, 显然, 这个用户的全部缓存数据操做的登陆用户Id都是同样的, 这时候equal定义就显得比较有用了.
- (void)delete {
IfUndefinedPrimaryKeyBreak;
[self deleteWithPredicate:[[HHPredicate predicateWithEqualKeys:[objectClass primaryKeys]] makePredicateWithObjcet:self]];
}
- (void)deleteWithEqualProperties:(NSArray *)properties {
[self deleteWithPredicate:[HHPredicate makePredicateWithObjcet:self equalProperties:properties]];
}
- (void)deleteWithPredicate:(NSPredicate *)predicate {
IfInvalidManagedObjectClassBreak;
dispatch_barrier_sync(NSObject.perfromQueue, ^{
NSManagedObject *managedObject;
if (predicate) {
managedObject = [managedObjectClass MR_findFirstWithPredicate:predicate inContext:[self class].saveContext];
}
if (managedObject) {
[managedObject MR_deleteEntityInContext:[self class].saveContext];
[[self class].saveContext MR_saveToPersistentStoreAndWait];
}
});
}
复制代码
+ (void)deleteAllMatchingPredicate:(NSPredicate *)predicate {
[self deleteAllMatchingPredicate:predicate completionHandler:nil];
}
+ (void)deleteAllMatchingPredicate:(NSPredicate *)predicate completionHandler:(void (^)())completionHandler {
IfInvalidManagedObjectClassBreak;
dispatch_barrier_async(NSObject.perfromQueue, ^{
[managedObjectClass MR_deleteAllMatchingPredicate:predicate inContext:self.saveContext];
[self.saveContext MR_saveToPersistentStoreAndWait];
DispatchCompletionHandlerOnMainQueue;
});
}
复制代码
数据删除最简单, 默认以Model声明的主键生成查询条件进行查询, 而后将查询到的NSManagedObject删除便可, 同步删除走dispatch_barrier_sync, 异步删除走dispatch_barrier_async.
单表的全部操做大概就是这样了, 整个工具的核心代码其实只有三百行代码不到的样子, 很是简单(目前我司的表结构都是单表, 多个表之间的联系经过外键维持. 单表这部分已经在上线项目中稳定运行了快一年了).
原本文章到这就算结尾了, 由于原理和实现已经说得很清楚了, 有什么需求均可以本身加, 但想一想关系这一块本身不用别人可能要用, 就顺便实现一下. 直接上代码吧:
@implementation Team
//@property (strong, nonatomic) Coach *coach;
//标识一下一对一关系, key是关系对象在本身这边的属性名, value是本身在关系对象的属性名
+ (NSDictionary *)oneToOneRelationship {
return @{@"coach" : @"team"};
}
@end
复制代码
@implementation Coach
//@property (strong, nonatomic) Team *team;
//标识一下一对一关系, key是关系对象在本身这边的属性名, value是本身在关系对象的属性名
+ (NSDictionary *)oneToOneRelationship {
return @{@"team" : @"coach"};
}
@end
复制代码
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject ignoreProperties:(NSSet *)ignoreProperties {
if (managedObject == nil) { return nil; }
//这里以Team - Coach举例 此时获取的是CoreTeam 它的一对一关系是CoreCoach
//此时的managedObject是CoreTeam 转换出的object是team
//managedObject.coach是CoreCoach 也就是下面的PropertyValue
id object = [self new];
HHClassInfo *classInfo = [NSObject managedObjectClassInfoWithObject:object];
NSDictionary *oneToOneRelationship = [(id)classInfo.cls respondsToSelector:@selector(oneToOneRelationship)] ? [classInfo.cls oneToOneRelationship] : nil;
for (HHPropertyInfo *property in classInfo.properties) {
if ([(id)managedObject respondsToSelector:property->_getter]) {
id propertyValue = [managedObject valueForKey:property->_getPath];
if (propertyValue != nil) {
switch (property->_type) {
//...其余属性 略
case HHPropertyTypeCustomObject: {
if ([ignoreProperties containsObject:property->_name]) { break; }
//1. 从 一对一关系表 中取出对应的属性名
NSString *oneToOneTargetName = oneToOneRelationship[property->_name];
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
!oneToOneTargetName ?: [ignorePropertyNames addObject:oneToOneTargetName];
//2. 将managedObject.coach(CoreCoach)转换成对应的Coach 由于此时的CoreCoach.team就是object自己 因此这里要在Coach转换时忽略team属性 否则就是死循环
propertyValue = [property->_cls objectWithManagedObject:propertyValue ignoreProperties:ignorePropertyNames];
if (oneToOneTargetName) {
id propertyValueClass = [propertyValue class];
if ([propertyValueClass respondsToSelector:@selector(oneToOneRelationship)] &&
[[propertyValueClass oneToOneRelationship].allKeys containsObject:oneToOneTargetName]) {
//3.将Model对应的 一对一关系属性 设置成本身
//即: object.coach.team = object(object == team)
[propertyValue setValue:object forKey:oneToOneTargetName];
}
}
//4. 将本身对应的 一对一关系属性 设置为Model
//即:object.coach = propertyValue(propertyValue == coach)
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, propertyValue);
} break;
//...其余属性 略
}
}
}
}
return object;
}
复制代码
Team *team = [Team instanceWithId:1];
Coach *coach = [Coach instanceWithId:1];
team.coach = coach;
//coach.team = team; 不须要这句 CoreData会根据声明自动创建一对一关系
[team save];
复制代码
首先咱们在Team和Coach双方都声明一下一对一关系, 这个关系声明其实就是双方对应关系的PropertyName, 以为绕的话, 直接打开CoreData图形化界面, 照着上面的剪头填写就好了, 站在CoreTeam的立场看, 它的关系属性名是coach, 而本身在对方的属性是team, 因此在Team.m里的关系描述就是@{@"coach" : @"team"}, 同理, Coach.m里面就是@{@"team" : @"coach"}.
仍以Team-Coach关系举例: 解析CoreTeam时会顺带解析CoreCoach, 而解析CoreCoach时又会去解析CoreTeam... 这就循环解析了, 因此咱们须要在第二层解析处破除一下这个循环. 另外, 由于一对一关系的两个对象实际上也就是循环引用, 会有内存泄漏, 直接使用NSManagedObject时咱们不须要关心这个泄漏, 由于它自己Context中不释放的缓存, 一出生就自带内存泄漏了. 可是咱们转换出来的Model不能这样搞, 在用完之后须要进行破环清理, 像这样:
[team clearRelationship];\\单个数据的关系清理
[teams clearRelationship];\\数组数据的关系清理 你不须要本身forin
复制代码
- (void)clearRelationship {
if ([self isKindOfClass:[NSDictionary class]]) {
[[(NSDictionary *)self allValues] clearRelationship];
} else if ([self isKindOfClass:[NSSet class]]) {
[[(NSSet *)self allObjects] clearRelationship];
} else if ([self isKindOfClass:[NSArray class]]) {
for (id object in (NSArray *)self) { [object clearRelationship]; }
} else {
NSDictionary *relationship = [self relationshipForObject:self];
[relationship enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
id relateObject = [self valueForKey:key];
NSDictionary *objcetRelationship = [self relationshipForObject:relateObject];
[objcetRelationship enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[[relateObject valueForKey:key] setValue:nil forKey:obj];
}];
}];
}
}
- (NSDictionary *)relationshipForObject:(id)object {
id cls = [object class];
NSDictionary *oneToOneRelationship = [cls respondsToSelector:@selector(oneToOneRelationship)] ? [cls oneToOneRelationship] : nil;
NSDictionary *oneToManyRelationship = [cls respondsToSelector:@selector(oneToManyRelationship)] ? [cls oneToManyRelationship] : nil;
if (oneToOneRelationship || oneToManyRelationship) {
NSMutableDictionary *relationship = [NSMutableDictionary dictionary];
[relationship setValuesForKeysWithDictionary:oneToOneRelationship];
[relationship setValuesForKeysWithDictionary:oneToManyRelationship];
return relationship;
}
return nil;
}
复制代码
clearRelationship的实现很简单, 直接根据一对一关系表将循环引用的部分置空就好了, 须要注意的是: 这个置空关系是从被引用的一方清空的, 而不是直接清空当前对象.
什么意思呢, 好比team生成时引用了coach, 直接设置team.coach.team = nil只能破除team和coach之间的循环引用, 若是coach自己还有其余的一对一属性, 那么被释放的只有team, coach和它本身的循环引用属性依然不会释放, 因此, 咱们要从coach端挨个释放.
一对多和一对一使用方法差很少, 这里以Team-Players举例, 代码以下:
@implementation Team
//设置数组元素为Model的属性对应的Model类
+ (NSDictionary *)containerPropertyKeypathsForCoreData {
return @{@"players" : @"Player"};
}
//设置一对多关系
+ (NSDictionary *)oneToManyRelationship {
return @{@"players" : @"team"};
}
@end
复制代码
@implementation Player
//设置一对一关系
+ (NSDictionary *)oneToOneRelationship {
return @{@"team" : @"players"};
}
@end
复制代码
//一对多关系解析
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject ignoreProperties:(NSSet *)ignoreProperties cacheTable:(NSMutableDictionary *)cacheTable {
if (managedObject == nil) { return nil; }
//此时的managedObject是CoreTeam 转换出的object是team
//managedObject.players是CorePlayer数组 也就是下面的PropertyValue
id object = [self new];
HHClassInfo *classInfo = [NSObject managedObjectClassInfoWithObject:object];
NSDictionary *containerPropertyKeypaths = [(id)classInfo.cls respondsToSelector:@selector(containerPropertyKeypathsForCoreData)] ? [classInfo.cls containerPropertyKeypathsForCoreData] : nil;
NSDictionary *oneToOneRelationship = [(id)classInfo.cls respondsToSelector:@selector(oneToOneRelationship)] ? [classInfo.cls oneToOneRelationship] : nil;
NSDictionary *oneToManyRelationship = [(id)classInfo.cls respondsToSelector:@selector(oneToManyRelationship)] ? [classInfo.cls oneToManyRelationship] : nil;
for (HHPropertyInfo *property in classInfo.properties) {
if ([(id)managedObject respondsToSelector:property->_getter]) {
id propertyValue = [managedObject valueForKey:property->_getPath];
if (propertyValue != nil) {
switch (property->_type) {
//...其余属性 略
//一对多关系解析
case HHPropertyTypeArray: {
//1. 从容器属性中 取出的容器元素Model对应的类名
//也就是team.players<Player *>
id objectsClass = NSClassFromString(containerPropertyKeypaths[property->_name]);
if (!objectsClass || [ignoreProperties containsObject:property->_name]) { break; }
//2. 从 一对多关系表 中取出对应的属性名
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
NSString *oneToManyTargetName = oneToManyRelationship[property->_name];
!oneToManyTargetName ?: [ignorePropertyNames addObject:oneToManyTargetName];
//3. forin解析CorePlayer数组为Player数组
NSMutableArray *objects = [NSMutableArray array];
for (id managedObj in propertyValue) {
id value = [objectsClass objectWithManagedObject:managedObj ignoreProperties:ignorePropertyNames cacheTable:cacheTable];
//4. 每一个Player都有一个队伍 即player.team = object(object == team)
[value setValue:object forKey:oneToManyTargetName];
if (value) { [objects addObject:value]; }
}
//5.每一个team都有多个的队员 即team.player = objects(objects是Player数组)
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, objects);
}
} break;
//...其余属性 略
}
}
}
}
return object;
}
复制代码
NSMutableArray *players = [NSMutableArray array];
for (int i = 1; i < 4; i++) {
[players addObject:[Player instanceWithId:i]];
}
Team *team = [Team instanceWithId:1];
team.players = players;//只设置任意一边的关系便可
[team save];
[team clearRelationship];//不用的时候记得清理
复制代码
和一对一关系同样, 咱们须要在各自的.m声明相应的关系, 站在team的角度看, 它和Player的关系是一对多的(一个队伍有多个队员), 因此在Team.m一对多(oneToManyRelationship)填上@{@"players" : @"team"}, 但站在player的角度看, 它和Team的关系是一对一(但一个队员只属于一只队伍)的, 因此在Player.m一对一(oneToOneRelationship)填上@{@"team" : @"players"}.
一对多的解析在Team这一方很简单, 只是简单的把CorePlayer数组转换成Player数组便可, 可是对Player这一方却须要加上一些小小的改动, 由于Player对Team是一对一的, 因此咱们从数据库取出不管多少个CorePlayer这些CorePlayer对应的CoreTeam都应该是同一个, 也就是说咱们不须要针对每一个被解析的Player都解析一次Team, 只须要在第一次解析后保存一下Team, 以后的解析直接使用便可, 相似于TableViewCell的重用, 大概是这样:
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject ignoreProperties:(NSSet *)ignoreProperties cacheTable:(NSMutableDictionary *)cacheTable {
if (managedObject == nil) { return nil; }
//Player-Team Player对Team是一对一, 可是Team对Player是一对多
//因此在设置关系是不能直接设置team.players = object 而是 team.players = @[object,...]
id object = [self new];
HHClassInfo *classInfo = [NSObject managedObjectClassInfoWithObject:object];
NSDictionary *oneToOneRelationship = [(id)classInfo.cls respondsToSelector:@selector(oneToOneRelationship)] ? [classInfo.cls oneToOneRelationship] : nil;
for (HHPropertyInfo *property in classInfo.properties) {
if ([(id)managedObject respondsToSelector:property->_getter]) {
id propertyValue = [managedObject valueForKey:property->_getPath];
if (propertyValue != nil) {
switch (property->_type) {
//...其余属性 略
case HHPropertyTypeCustomObject: {
if ([ignoreProperties containsObject:property->_name]) { break; }
//1.从 一对一关系表 中取出对应的关系表
NSString *oneToOneTargetName = oneToOneRelationship[property->_name];
//2.以NSManagedObject的地址判断重用表中是否有可重用数据
NSString *cachedObjectKey = [NSString stringWithFormat:@"%p", propertyValue];
if ([cacheTable.allKeys containsObject:cachedObjectKey]) {
propertyValue = cacheTable[cachedObjectKey];
} else {
//3.没有可重用数据 进入解析流程 并将解析结果放入重用表
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
!oneToOneTargetName ?: [ignorePropertyNames addObject:oneToOneTargetName];
propertyValue = [property->_cls objectWithManagedObject:propertyValue ignoreProperties:ignorePropertyNames cacheTable:cacheTable];
!propertyValue ?: [cacheTable setObject:propertyValue forKey:cachedObjectKey];
}
if (oneToOneTargetName) {
//4.若是关系的另外一方也是一对一关系直接设置便可
id propertyValueClass = [propertyValue class];
if ([propertyValueClass respondsToSelector:@selector(oneToOneRelationship)] &&
[[propertyValueClass oneToOneRelationship].allKeys containsObject:oneToOneTargetName]) {
[propertyValue setValue:object forKey:oneToOneTargetName];
} else if ([propertyValueClass respondsToSelector:@selector(oneToManyRelationship)] && [[propertyValueClass oneToManyRelationship].allKeys containsObject:oneToOneTargetName]) {
//5.若是关系的另外一方是一对多关系须要设置本身到它的数组中
NSMutableArray *objects = [NSMutableArray arrayWithArray:[propertyValue valueForKey:oneToOneTargetName]];
[objects addObject:object];
[propertyValue setValue:objects forKey:oneToOneTargetName];
}
}
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, propertyValue);
} break;
//...其余属性 略
}
}
}
}
return object;
}
复制代码
多对多关系其实就是两个一对多的组合, 直接在HHPropertyTypeArray的代码基础上稍做修改便可:
case HHPropertyTypeArray: {
id objectsClass = NSClassFromString(containerPropertyKeypaths[property->_name]);
if (!objectsClass || [ignoreProperties containsObject:property->_name]) { break; }
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
NSString *oneToManyTargetName = oneToManyRelationship[property->_name];
!oneToManyTargetName ?: [ignorePropertyNames addObject:oneToManyTargetName];
NSMutableArray *objects = [NSMutableArray array];
for (id managedObj in propertyValue) {
NSString *cachedObjectKey = [NSString stringWithFormat:@"%p", managedObj];
id objValue = cacheTable[cachedObjectKey];;
if (!objValue) {
objValue = [objectsClass objectWithManagedObject:managedObj ignoreProperties:ignorePropertyNames cacheTable:cacheTable];
!objValue ?: [cacheTable setObject:objValue forKey:cachedObjectKey];
}
if (objValue) {
[objects addObject:objValue];
if (oneToManyTargetName) {
id objValueClass = [objValue class];
if ([objValueClass respondsToSelector:@selector(oneToOneRelationship)] &&
[[objValueClass oneToOneRelationship].allKeys containsObject:oneToManyTargetName]) {
[objValue setValue:object forKey:oneToManyTargetName];
} else if ([objValueClass respondsToSelector:@selector(oneToManyRelationship)] && [[objValueClass oneToManyRelationship].allKeys containsObject:oneToManyTargetName]) {
NSMutableArray *objValueObjects = [NSMutableArray arrayWithArray:[objValue valueForKey:oneToManyTargetName]];
[objValueObjects addObject:object];
[objValue setValue:objValueObjects forKey:oneToManyTargetName];
}
}
}
}
((void (*)(id, SEL, id))(void *) objc_msgSend)(object, property->_setter, objects);
} break;
复制代码
我的比较懒散, 目前我只实现了一些本身用得上的基本功能, 还有不少能够优化的点, 好比:
感受写了好多字啊, 其实核心代码只有300行左右... 最后说一点吧, 由于这套工具是 对象映射+CoreData操做, 可能有些朋友会担忧有效率问题, 其实不用担忧, 对象映射的效率在以前的文章我有提过, 很快! 最耗时的实际上是CoreData自己的存储操做, 但这部分显然是没法优化的(可能之后会变好). 因此, 若是你接受不了原生的存储速度的话, 你应该放弃CoreData, 拥抱SQLite/Realm...