從簡書遷移到掘金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...