前言
本文并不是CoreData從入門到精通之類的教程, 并不會涉及到過多的原理概念描述, 而是介紹如何讓CoreData的使用變得更加簡單明了, 方便親民. 全文約六千字, 預(yù)計花費閱讀時間15分鐘.
簡書賬號停止維護, 提問/討論請移步掘金賬號
目錄
- 這是什么以及如何使用
- 原理概述
- 實現(xiàn)細節(jié): 單表的增刪改查
- 實現(xiàn)細節(jié): 一對一關(guān)系
- 實現(xiàn)細節(jié): 一對多關(guān)系
- 實現(xiàn)細節(jié): 多對多關(guān)系
一. 這是什么以及如何使用
大概是去年夏天, 因為要做數(shù)據(jù)緩存開始使用MagicalRecord. 在和同事們使用了一段時間后, 發(fā)現(xiàn)在復(fù)雜的業(yè)務(wù)場景中單純的MagicalRecord應(yīng)用起來還是略有些麻煩, 于是就在它的基礎(chǔ)上再封裝了一層, 最后實現(xiàn)的結(jié)果還是比較滿意的. 因為比較實用, 而且原理很簡單, 索性就開篇博客把這個工具放出來.
這里以一段和同事A(A沒有任何CoreData方面的開發(fā)經(jīng)驗, 所以比較有代表性)的對話描述一下這個工具如何使用:
我: 東西寫完了, A啊, 你過來看看, 我給你說說怎么用. 假設(shè)你現(xiàn)在要存一個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里面的字段就是你要存的那些屬性名, 注意: 名字和類型盡量一一對應(yīng).
現(xiàn)在你有了一個本地Model類Snack和一個NSManagedObject類CoreSnack, 他們兩的關(guān)系就相當于本地Model和網(wǎng)絡(luò)數(shù)據(jù)的Protobuf/JSon, 只不過這次他們的關(guān)系是雙向的, 我們不僅可以將CoreSnack轉(zhuǎn)換成Snack, 也可以將Snack轉(zhuǎn)換成CoreSnack.
A: 額, JSon/Protobuf/ManagedObject轉(zhuǎn)Model很簡單, 直接用之前寫的轉(zhuǎn)換工具就行了, Model怎么轉(zhuǎn)ManagedObject? Model又不知道自己對應(yīng)的ManagedObject類是哪一個? 還有, 難道每次轉(zhuǎn)換都存一個新數(shù)據(jù)進去? 那不是好多重復(fù)數(shù)據(jù)?
我: 嗯, 你說的很對, 所以你需要在Snack里面聲明它對應(yīng)的ManagedObject是哪個, 還有這個ManagedObject的主鍵, 默認情況下, 我會用主鍵去重. 像這樣:
@implementation Snack
#pragma mark - CoreData
//Model和CoreData對應(yīng)關(guān)系
+ (Class)managedObjectClass {
return NSClassFromString(@"CoreSnack");
}
//主鍵 (key是Model屬性名, value是CoreData字段名, 一般情況下是一樣的, 聲明成字典只是以防萬一)
+ (NSDictionary *)primaryKeys {
return @{@"snackId" : @"snackId"};
}
@end
A: 奧, 行吧. 那這些東西我都聲明好了的話, 我怎么存東西, 要自己調(diào)用CoreData的接口嗎?
我: 不需要你調(diào)用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數(shù)組的話, 怎么搞? forin嗎?
我: 不行, 每次存儲都是要訪問數(shù)據(jù)庫的, 用forin的話會多次訪問數(shù)據(jù)庫, 很耗時的! 如果你要存一個Model數(shù)組, 用下面這個接口:
NSMutableArray *snacks = [NSMutableArray array];
for (int i = 1; i < 9; i++) {
Snack *snack = [Snack instanceWithId:i];
[snacks addObject:snack];
}
[Snack saveObjects:snacks]; //異步存儲無回調(diào)接口(數(shù)組存儲只提供異步接口)
[Snack saveObjects:snacks completionHandler:^{
}]; //異步存儲有回調(diào)接口
A: 你這個好像只能存普通數(shù)據(jù)類型, 那如果我的Model有幾個屬性本身也是Model, 有的屬性也有對應(yīng)的ManagedObject, 有的沒有, 有的甚至是數(shù)組, 怎么辦?
我: 沒有關(guān)系, 也是一樣的用法, 你只管設(shè)置, 接口會幫你存好的, 但是如果你的屬性里面有映射到ManagedObject的Model數(shù)組的話, 你最好用異步存儲的接口:
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: 嗯... 東西存進去了, 那怎么取出來?
我: 因為存東西是分單個存儲和數(shù)組存儲, 所以取東西也給了兩組接口, 我一組一組給你演示. 先是單個查詢的接口:
[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 參數(shù)即可
數(shù)組查詢接口:
// 查詢接口大體分兩種: 條件查詢和分頁查詢(這里列的都是同步查詢接口)
// 條件查詢
[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 參數(shù)即可
我: 另外, 如果從CoreData取數(shù)據(jù)的時候, 某個屬性也是一個Model數(shù)組, 記得在這條數(shù)據(jù)對應(yīng)的Model中里面聲明屬性數(shù)組里面是什么Model, 這點和JSon/Protobuf是一樣的, 比如上面的Worker有個snacks數(shù)組屬性, 數(shù)組元素也是Model, 你就要在Worker里面這樣聲明一下:
@implementation Worker
+ (Class)managedObjectClass {
return NSClassFromString(@"CoreWorker");
}
+ (NSDictionary *)containerPropertyKeypathsForCoreData {
return @{@"snacks" : @"Snack"};
}
@end
A: 行啦, 我知道了. 現(xiàn)在有存有取, 那改數(shù)據(jù)怎么改, 是不是要先查詢, 然后改數(shù)據(jù), 改完了再存進去?
我: 嗯, 邏輯是這個邏輯, 但是不用你寫這些代碼, 你只需要提供查詢條件就行了, 像這樣:
//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 {
//當你需要改這條數(shù)據(jù)的時候
// 法1
Snack *snack = [Snack new]; //新建
snack.name = @"xxx"; //要改的部分直接賦值
snack.size = 999; //要改的部分直接賦值
[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
//內(nèi)部會以你提供的Predicate進行查詢 你設(shè)置的值進行更改 最后進行更新存儲
// 法2
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //設(shè)置要改的那條記錄對應(yīng)的主鍵值
snack.name = @"xxx"; //要改的部分直接賦值
snack.size = 999; //要改的部分直接賦值
[snack save];
//如果不寫查詢條件 默認會以主鍵為作為查詢條件
//等同于[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
// 法3 和 法2 類似 不過這次會以提供的查詢數(shù)組生成查詢條件
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //設(shè)置要改的那條記錄對應(yīng)對應(yīng)查詢值
snack.otherProperty = yyy;//設(shè)置要改的那條記錄對應(yīng)查詢值
snack.name = @"xxx"; //要改的部分直接賦值
snack.size = 999; //要改的部分直接賦值
[snack saveWithEqualProperties:@[@"snackId", @"otherPrimaryKey"]];
//如果設(shè)置了查詢條件數(shù)組 會以你提供的查詢數(shù)組生成查詢條件
//等同于[snack saveWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123 && otherProperty = yyy"]];
//note: 在數(shù)據(jù)修改時, 直接給某個值設(shè)置為nil或者0對應(yīng)的CoreData在數(shù)據(jù)庫中不會有任何改變 比如snack.snackId = 0和snack.name = nil都是無效的
//如果你確實想將某個字段設(shè)置為空, 那就設(shè)置對應(yīng)的空值, 比如snack.name = @""和snack.snackId = CDZero(因為數(shù)字沒有空值, 所以我聲明了一個保留字)
//同理 以上的同步操作接口加上 completionHandler 就是對應(yīng)的異步操作接口
}
A: 有個問題, 這個接口看起來和存儲接口好像啊. 如果我給的查詢條件有問題, 最后沒有查到數(shù)據(jù)庫里面的數(shù)據(jù), 或者數(shù)據(jù)庫里面根本就沒有這條數(shù)據(jù), 會怎么樣?
我: 查詢查不到的話, 那就直接存一條數(shù)據(jù)進去咯, 數(shù)據(jù)存儲其實調(diào)用的就是數(shù)據(jù)更新接口, 不然你以為我怎么去重的?
A: 額, 我擔(dān)心我寫錯查詢條件, 能不能查不到就不存?
我: 可以啊, 加個參數(shù)或者把save和update分開就行了, 不過我懶, 沒實現(xiàn), 你需要就自己實現(xiàn)咯!
接下來是數(shù)組的批量更新接口:
//將你想要更新的批量數(shù)據(jù) 放到一個數(shù)組里面 然后通過通過HHPredicate(注意: 不是NSPredicate)設(shè)置查詢條件
//HHPredicate定義了查詢條件的 ==(equalKeys) 關(guān)系和 in(containKeys) 關(guān)系 批量更新會根據(jù)這些關(guān)系去進行數(shù)據(jù)查詢
//HHPredicte的關(guān)系鍵值對和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"}]];
//內(nèi)部會生成一個NSPredicate = [NSPredicate predicateWithFormat:@"snackId in {1, 2, 3, 4...}"]
//這里的例子沒有==關(guān)系, 如果是復(fù)合鍵做主鍵的話, 會頻繁用到equalKeys, 或者想優(yōu)化查詢速度也可以設(shè)置equalKeys
//比如設(shè)置了equalKeys{@"xxxProperty" : @"xxx"}就會生成[NSPredicate predicateWithFormat:@"xxxProperty = xxx && snackId in {1, 2, 3, 4...}"]
//法 2 因為一般設(shè)置Model的屬性名和CoreData的字段名都是一樣的 所以直接給個便利的方法出來
[Snack saveObjects:snacks checkByPredicate:[HHPredicate predicateWithEqualProperties:nil containProperties:@[@"snackId"]]];
批量更新和單個更新一樣, 查詢條件查不到的那部分數(shù)據(jù)就會被認為是普通存儲, 會新建這部分的數(shù)據(jù)并存儲到數(shù)據(jù)庫中.
A: 只剩下刪除了, 這部分是什么樣子的?
我: 刪除和更新接口差不多, 不過要簡單多了, 像這樣:
// 法1
Snack *snack = [Snack new]; //新建
[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]]; //內(nèi)部會以你提供的Predicate進行查詢 然后刪除
// 法2
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //設(shè)置要刪除的那條記錄對應(yīng)的主鍵值
[snack delete]; //如果不寫查詢條件 默認會以主鍵為作為刪除條件
// 等同于[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"snackId = 123"]];
// 法3
Snack *snack = [Snack new]; //新建
snack.snackId = 123; //設(shè)置要刪除那條記錄的滿足條件1
snack.otherPrimaryKey = xxx;//設(shè)置要刪除那條記錄的滿足條件2
[snack deleteWithEqualProperties:@[@"snackId", @"otherPrimaryKey"]];
// 等同于[snack deleteWithPredicate:[NSPredicate predicateWithFormat:@"otherPrimaryKey = xxx && snackId = 123"]];
批量刪除更簡單:
[Snack deleteAll]; //全部刪除
[Snack deleteAllMatchingPredicate:[NSPredicate predicateWithFormat:@"snackId <= 10"]]; //刪除滿足條件的部分
A: 增刪改查算是齊了, 但是我聽說CoreData是線程不安全的, 那我在使用的時候需要注意什么? 還有多線程的數(shù)據(jù)同步呢?
我: 多線程和數(shù)據(jù)同步的問題不需要你關(guān)心, 你只管記住上面的這些接口就行了, 比如下面的寫法是完全沒問題的:
- (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), ^{
//查詢操作會等到最近的一次存儲/刪除/更新操作完成后才執(zhí)行(即數(shù)據(jù)同步)
NSArray *snacks = [Snack findAllSortedBy:@"snackId" ascending:YES withPredicate:[NSPredicate predicateWithFormat:@"snackId >= 100 && snackId < 109"]];
//子線程查詢的數(shù)據(jù)拿到主線程去用也完全沒問題(即安全的跨線程訪問)
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: 那敢情好, 所以現(xiàn)在我要操作CoreData數(shù)據(jù)只需要做三件事:
1.根據(jù)業(yè)務(wù)需求新建本地Model同時在.xcdatamodeld里面新建與Model對應(yīng)的Entity, 并設(shè)置相應(yīng)字段.
2.在本地Model內(nèi)部聲明和對應(yīng)的NSManagedObject類的映射關(guān)系.
3.調(diào)用相應(yīng)接口進行增刪改查.
二. 原理概述
在說實現(xiàn)原理之前, 先看看現(xiàn)有的CoreData用起來有哪些缺點, 或者說麻煩的地方.
我們都知道, 在對CoreData做任何操作時, 都是通過一個Context來完成的, Context內(nèi)部管理著和自己相關(guān)的ManagedObject, 對外則提供各種操作這些對象的相應(yīng)接口, 它的本質(zhì)其實是處在應(yīng)用和數(shù)據(jù)庫之間的一層全局緩存(在Context和實際的數(shù)據(jù)庫之間還有一層NSPersistentStoreCoordinator, 但這和本文要講的東西聯(lián)系不大, 略去不談).
緩存帶來的效益是可觀的, 在緩存命中的情況下, 在內(nèi)存中操作數(shù)據(jù)肯定比直接訪問數(shù)據(jù)庫效率要高, 這在數(shù)據(jù)讀寫頻繁或者數(shù)據(jù)量大時尤為有效. 但事有利弊, 緩存帶來了效率的提升同時也引出了一些問題. 比較突出的有兩點:老是被人詬病的內(nèi)存問題以及因緩存數(shù)據(jù)同步而引發(fā)的實際使用時操作繁瑣復(fù)雜.
第一點幾乎是無解的(但iPhone的配置一直在提升, 所以這個問題現(xiàn)在也不算什么問題了), 我們主要聊聊第二點. 共享數(shù)據(jù)在多線程操作的情況下勢必是要做數(shù)據(jù)同步的(不然我在這個線程用你在那個線程改, 你在這個線程改他在那個線程刪, 數(shù)據(jù)不是全亂套了), Context作為一個全局緩存自然也不例外. 系統(tǒng)對此采取的措施是: 為Context配置相應(yīng)的并發(fā)操作類型(NSManagedObjectContextConcurrencyType). 這個并發(fā)操作類型定義了Context的數(shù)據(jù)操作規(guī)范: 從Context讀取的數(shù)據(jù)只能在當前獲取數(shù)據(jù)的線程中訪問, 而更改數(shù)據(jù)的操作只能在Context對應(yīng)的操作隊列中執(zhí)行(MainConcurrencyType的操作隊列就是UI線程, 而PrivateConcurrencyType的操作隊列由系統(tǒng)自建, 對外不可見, 外部只能通過performBlock將操作添加到這個隊列執(zhí)行). 這套操作規(guī)范解決了緩存內(nèi)部的數(shù)據(jù)同步問題, 但卻間接引起了更加麻煩的問題: 緩存間的數(shù)據(jù)同步.
我們來看看緩存內(nèi)數(shù)據(jù)同步是如何引出緩存間數(shù)據(jù)同步問題的:
1.數(shù)據(jù)更改操作只能在Context對應(yīng)的操作隊列執(zhí)行, 如果此時的Context對應(yīng)的操作隊列是主線程, 耗時的數(shù)據(jù)操作就會卡UI, 這是不能接受的.
2.耗時操作不要在UI線程做, 于是通常我們會至少建立一個子線程的Context來做耗時操作.
3.多個Context其實也就是多個緩存塊, 它們之間是各自獨立毫無聯(lián)系的, 這意味著在一個Context進行的任何操作另一個Context是不知情的, 所以你不能指望在子線程存儲后再去UI線程讀取, 沒有用. 即使以后允許這樣做, 你也必須等到子線程數(shù)據(jù)更新完成后才能到主線程進行查詢, 否則就是數(shù)據(jù)錯亂. 另外, 因為從Context讀取的數(shù)據(jù)只能在當前獲取數(shù)據(jù)的線程中訪問, 所以也不能從子線程讀取數(shù)據(jù)后直接傳遞到UI線程使用, 沒有用...
4.按下葫蘆浮起瓢, 為了解決緩存內(nèi)數(shù)據(jù)同步, 我們不得不處理緩存間數(shù)據(jù)同步, 而且, 這個過程系統(tǒng)能提供的幫助十分有限...(緩存間數(shù)據(jù)同步目前業(yè)界有許多方案, 諸如Context操作完成后發(fā)通知讓其他Context進行數(shù)據(jù)同步, 兩層/三層基于child/parentContext的設(shè)計等等, 另外還涉及到數(shù)據(jù)傳遞和數(shù)據(jù)沖突解決, 內(nèi)容較多且與本文無關(guān), 這里不做細表)
哎, 光是各種Coordinator/Context/Objcet/ConcurrencyType對象之間的關(guān)系就夠麻煩的了, 現(xiàn)在還要自己搞數(shù)據(jù)同步, 這讓很多剛接觸CoreData的同學(xué)熱情大減, 紛紛表示CoreData太復(fù)雜了, 還是用SQLite/Realm吧...
好了, 上面長篇大論一頓分析, 現(xiàn)在終于講到本文的目的了: 如何實現(xiàn)一套支持線程安全且簡單易用的CoreData工具?
回答這個問題我們需要把視線挪到分析緩存間數(shù)據(jù)同步的第一點, 然后想一想: 如果一開始Context就支持在子線程操作數(shù)據(jù)同時也能在UI線程訪問數(shù)據(jù)而且還自帶數(shù)據(jù)同步特效, 后面的一系列問題不就都沒有了嗎?
把這個想法當成一個需求, 我們來做一個簡單的可行性分析. 這個需求一共三點:子線程操作數(shù)據(jù), UI線程訪問數(shù)據(jù), 多線程數(shù)據(jù)同步. 第一點很簡單, 直接設(shè)置Context并發(fā)操作類型為PrivateConcurrencyType就行了, 第三點也很簡單, 多線程數(shù)據(jù)同步就是加鎖嘛, 讀寫頻繁的話把鎖換成dispatch_barrier_async/sync和dispatch_async/sync的組合就行了.
第二點就麻煩一點了, 因為NSManagedObject是和Context強關(guān)聯(lián)的, 想要脫離Context的線程限制進行數(shù)據(jù)訪問是不太現(xiàn)實的. 對此, 我們需要繞一個小彎, 即在可訪問的線程中將NSManagedObject的值映射到一個可以跨線程訪問的對象上(也就是我們的Model), 在待使用線程使用這個映射對象而不是NSManagedObject, 借此解決跨線程訪問的問題. 最后, 當我們在需要對數(shù)據(jù)進行任何修改時, 先將映射對象還原相應(yīng)的NSManagedObject, 再通過Context去到子線程執(zhí)行對應(yīng)的操作. 饒了一個彎, 不過還好, 因為最麻煩的互相轉(zhuǎn)換的工具很久以前就已經(jīng)實現(xiàn)了, 直接把之前寫的Protobuf解析器簡單改改就可以用了.
原理大概就是這樣了, 概括起來其實我們只做了兩件事情:
1.通過dispatch_barrier_async/sync和dispatch_async/sync的組合做多線程數(shù)據(jù)同步.
2.通過NSManagedObject和Model之間的互相轉(zhuǎn)換做跨線程訪問和CoreData數(shù)據(jù)操作.
進一步的, 這套路其實算是ORM的變種實現(xiàn)(CoreData本身其實就是ORM的一種實現(xiàn), 默認映射關(guān)系是SQLite--NSManagedObject). 理論上, 我們只要換一個數(shù)據(jù)轉(zhuǎn)換工具, 重寫一下數(shù)據(jù)操作接口, 那么下層即使換掉CoreData改用SQLite/Realm/xxx也是一樣的.
三. 實現(xiàn)細節(jié): 單表的增刪改查
上面說到的原理很簡單, 具體實現(xiàn)起來也很簡單, 這里我就簡單貼貼代碼, 主要說一下細節(jié)問題就好.
- 查詢
//某個分頁同步查詢接口
+ (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];
}
...若干異步查詢接口
查詢的實現(xiàn)很簡單, 通過Model聲明的映射關(guān)系拿到NSManagedObject類, 然后執(zhí)行查詢, 將查詢結(jié)果轉(zhuǎn)換成Model傳遞出來即可, 因為這部分邏輯都是一樣的, 所以就直接寫出一系列通用方法, 各個查詢接口調(diào)用這些通用方法即可. 具體的方法實現(xiàn)如下:
//某個同步查詢通用方法
+ (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;
});
});
}
所有的查詢方法最后都會走到一個解析方法去做數(shù)據(jù)轉(zhuǎn)換, 該方法如下:
+ (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;
//...各種格式的數(shù)據(jù)賦值
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;
//...各種格式的數(shù)據(jù)賦值
}
}
}
}
return object;
}
這塊的邏輯和Protobuf解析差不多(Protobuf解析的具體邏輯), 無非就是各種數(shù)據(jù)格式的賦值, 自定義類和數(shù)組屬性的特殊處理. 不過和之前的解析不同, 在數(shù)組這塊多了一個字符串判斷, 然后才是數(shù)組屬性的解析, 簡單解釋下:
CoreData默認是不能存數(shù)組的, 要存數(shù)組需要走transformable然后自己手寫序列化, 這個過程其實也不過是一次中間值轉(zhuǎn)化, 存儲時將數(shù)組轉(zhuǎn)化為Data, 獲取時將Data轉(zhuǎn)化回數(shù)組. 但我們這套工具的目的是使用簡單, 所以不想讓使用者關(guān)心這些東西, 默認我會在存儲時將數(shù)組通過逗號分割拼裝成字符串放進數(shù)據(jù)庫, 然后在獲取時將字符串再解析回數(shù)組. 另外, transformable的存儲方式是不支持條件查詢的, 因為它是存儲的是Data, 無從比對, 但分割字符串的方式顯然是支持條件查詢的, 而且還不用使用者寫任何多余代碼. (當然, transformable還有其他的應(yīng)用場景, 比如存UIImage之類的對象, 這種情況還是需要自己手寫序列化的, 但這種情況不屬于數(shù)組, 屬于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];
}
}
為了方便, 添加數(shù)據(jù)和修改數(shù)據(jù)走的是一個接口, 同時添加和更新還分為單個操作和批量操作, 這里先介紹單個操作. 代碼中的注釋應(yīng)該很明顯了, 我們直接跳到配置并存儲數(shù)據(jù)的部分:
- (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;
}
//數(shù)組且數(shù)組內(nèi)只是普通數(shù)據(jù)類型
id element = [propertyValue firstObject];
if ([element isKindOfClass:[NSString class]] ||
[element isKindOfClass:[NSNumber class]]) {
propertyValue = [propertyValue componentsJoinedByString:@","];
} else {//數(shù)組且數(shù)組內(nèi)也是NSManagedObject
//...這部分和批量操作差不多 下文介紹
}
}
if (propertyValue) { [self setValue:propertyValue forKeyPath:property->_getPath]; }
}
}
}
}
通過Model配置NSManagedObject也就是Model解析的逆操作, 而NSManagedObject本身只支持KVC的方式進行賦值, 所以比起Model解析部分的各種MsgSend和數(shù)據(jù)格式判斷要簡單的多, 這里我只介紹一點: 數(shù)據(jù)初始化和數(shù)據(jù)合并.
當我們在對CoreData做修改操作時其實就是一次數(shù)據(jù)合并操作, 我們將此時需要修改的值覆蓋數(shù)據(jù)庫原有的值, 但是不需要修改的部分是不變的, 這可以看做是兩個Model各自將一部分數(shù)據(jù)進行組裝生成第三個合并Model. 在這套工具中, 第一個Model就是數(shù)據(jù)庫中原有的值, 第二個Model就是我們想要修改數(shù)據(jù)的的值, 合并的邏輯是將第二個Model中不為空的部分(也就是我們設(shè)置修改的部分)賦值給第一個Model, 然后將更新后的Model存回數(shù)據(jù)庫. 但這有一個問題, 那就是如果我就是想將數(shù)據(jù)庫中的值置空怎么辦?
目前我的處理是, 如果你確實想將某個值置空, 那就傳對應(yīng)的空值而不是nil, 因為直接設(shè)置nil是不做覆蓋的. 比如, 你想將某個字符串屬性置空, 那就傳@"", 數(shù)組就傳@[], 如果你想將某個數(shù)字置0, 那就傳CDZero(這是我聲明的一個保留字, 因為數(shù)字在KVC獲取時是不可能為空的, 拿到的都是0), 這些空值在被覆蓋后重新獲取時會被判定為nil, 以達到置空的目的.
當我們向CoreData增加數(shù)據(jù)時其實做的是數(shù)據(jù)初始化, 但因為數(shù)據(jù)初始化是數(shù)據(jù)合并的子集, 所以數(shù)據(jù)初始化就直接用數(shù)據(jù)合并的邏輯了.
單個數(shù)據(jù)添加和修改說完了, 接下來看看批量添加和修改:
//批量添加/更新便利方法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];
}
//實際執(zhí)行批量添加/更新的方法
+ (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. 根據(jù)查詢條件的查詢數(shù)據(jù)中已有的部分
NSArray *managedObjects = [managedObjectClass MR_findAllWithPredicate:[predicate makePredicateWithObjcets:objects] inContext:self.saveContext];
//2. 以 HHPredicate 中的唯一標識符規(guī)則從NSManagedObject處生成一個的標識符X數(shù)組
NSMutableArray *managedObjectIdentifierArray = [NSMutableArray array];
for (NSManagedObject *managedObject in managedObjects) {
id managedObjectIdentifier = [predicate identifierWithManagedObjcet:managedObject];
if (managedObjectIdentifier) {
[managedObjectIdentifierArray addObject:managedObjectIdentifier];
}
}
//3. 遍歷需要更新/存儲的Model數(shù)組
for (id object in objects) {
//3.1 以 HHPredicate 中的唯一標識符規(guī)則從Model處也生成一個標識符Y
NSManagedObject *managedObject;
id objectIdentifier = [predicate identifierWithObjcet:object];
//3.2 如果標識符Y和步驟2中生成的標識符X匹配, 說明它是已經(jīng)存在于數(shù)據(jù)庫中, 即修改操作
if ([managedObjectIdentifierArray containsObject:objectIdentifier]) {
managedObject = [managedObjects objectAtIndex:[managedObjectIdentifierArray indexOfObject:objectIdentifier]];
} else {//3.3 標識符Y和標識符X不匹配, 說明是添加操作
managedObject = [managedObjectClass MR_createEntityInContext:self.saveContext];
}
//3.4 根據(jù)Model配置managedObject(新建的或者數(shù)據(jù)庫本來就有的)
[managedObject configWithObject:object];
}
//4. 將添加/修改提交到數(shù)據(jù)庫
[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;
}
可以看見, 批量操作比單個操作要復(fù)雜一些, 因為批量操作中常常同時存在添加和更新. 舉個例子: 數(shù)據(jù)庫中第一天存了一些用戶好友, 第二天可能這些好友有些改了昵稱/頭像/個性簽名然后用戶自己在網(wǎng)頁端又新添加了一些好友, 此時我們直接調(diào)用接口拉取下來的第一頁幾十條數(shù)據(jù)中就肯定有部分是修改, 有部分是添加的, 如果讓使用者自己查詢?nèi)缓髤^(qū)分哪部分是添加, 哪部分是修改, 無疑增加了使用復(fù)雜度, 所以這些東西我也選擇由工具自己來做而不是拋給使用者. 這也是為什么從一開始, 數(shù)據(jù)更新和數(shù)據(jù)存儲就是走的一個接口的原因, 因為批量操作遇到這種情況簡直不要太多.
我們通過HHPredicate來生成去重的查詢條件, 同時還生成Model和NSManagedObject的唯一標識符, 那么這個唯一標識符是怎么生成的呢? 其實很簡單, HHPredicate定義了equal(==)和contain(in)關(guān)系, equal中的字段定義了整個待操作的數(shù)組Model值相同的字段, 通常這部分用作縮小查詢范圍加快查詢速度, 是可有可無的. contain定義了數(shù)組中每個Model都不相同的字段, 是必須要有的, 通常這個字段就是主鍵. 舉個例子: 比如我們要存一個User數(shù)組, 這個數(shù)組中每個User的主鍵UserId肯定都是不同的, 當然, 其他的字段諸如年齡, 名字可能也不同, 但是我們只需要一個字段就足夠標識了, 所以此時contain定義就填UserId. 那equal定義呢? 你可以不填, 但是如果這些User確實有一個字段全都一樣, 比如全都是xxx公司的員工, 那你可以在equal定義填上xxx公司, 這樣的查詢會比較快.
equal定義在復(fù)合鍵做主鍵時特別有用, 因為很多時候批量操作只有一部分是完全不同的, 另一部分都是一樣的. 比如三年二班的學(xué)生, 他們的學(xué)號通常是不同的1到100, 但是班級都是三年二班. 單憑學(xué)號不足以唯一標識一個學(xué)生, 畢竟其他班也有1到100的學(xué)號, 但是加上班級后就可以了. 在實際使用中, 通常一個手機可以有若干個賬號, 每個賬號都有若干好友/作品..., 單憑好友/作品Id是不足以做唯一標識符的, 還必須加上當前登錄的用戶Id, 顯然, 這個用戶的所有緩存數(shù)據(jù)操作的登錄用戶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;
});
}
數(shù)據(jù)刪除最簡單, 默認以Model聲明的主鍵生成查詢條件進行查詢, 然后將查詢到的NSManagedObject刪除即可, 同步刪除走dispatch_barrier_sync, 異步刪除走dispatch_barrier_async.
單表的所有操作大概就是這樣了, 整個工具的核心代碼其實只有三百行代碼不到的樣子, 非常簡單(目前我司的表結(jié)構(gòu)都是單表, 多個表之間的聯(lián)系通過外鍵維持. 單表這部分已經(jīng)在上線項目中穩(wěn)定運行了快一年了).
四. 實現(xiàn)細節(jié): 一對一關(guān)系
本來文章到這就算結(jié)尾了, 因為原理和實現(xiàn)已經(jīng)說得很清楚了, 有什么需求都可以自己加, 但想想關(guān)系這一塊自己不用別人可能要用, 就順便實現(xiàn)一下. 直接上代碼吧:
@implementation Team
//@property (strong, nonatomic) Coach *coach;
//標識一下一對一關(guān)系, key是關(guān)系對象在自己這邊的屬性名, value是自己在關(guān)系對象的屬性名
+ (NSDictionary *)oneToOneRelationship {
return @{@"coach" : @"team"};
}
@end
@implementation Coach
//@property (strong, nonatomic) Team *team;
//標識一下一對一關(guān)系, key是關(guān)系對象在自己這邊的屬性名, value是自己在關(guān)系對象的屬性名
+ (NSDictionary *)oneToOneRelationship {
return @{@"team" : @"coach"};
}
@end
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject ignoreProperties:(NSSet *)ignoreProperties {
if (managedObject == nil) { return nil; }
//這里以Team - Coach舉例 此時獲取的是CoreTeam 它的一對一關(guān)系是CoreCoach
//此時的managedObject是CoreTeam 轉(zhuǎn)換出的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. 從 一對一關(guān)系表 中取出對應(yīng)的屬性名
NSString *oneToOneTargetName = oneToOneRelationship[property->_name];
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
!oneToOneTargetName ?: [ignorePropertyNames addObject:oneToOneTargetName];
//2. 將managedObject.coach(CoreCoach)轉(zhuǎn)換成對應(yīng)的Coach 因為此時的CoreCoach.team就是object本身 所以這里要在Coach轉(zhuǎn)換時忽略team屬性 不然就是死循環(huán)
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對應(yīng)的 一對一關(guān)系屬性 設(shè)置成自己
//即: object.coach.team = object(object == team)
[propertyValue setValue:object forKey:oneToOneTargetName];
}
}
//4. 將自己對應(yīng)的 一對一關(guān)系屬性 設(shè)置為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會根據(jù)聲明自動建立一對一關(guān)系
[team save];
首先我們在Team和Coach雙方都聲明一下一對一關(guān)系, 這個關(guān)系聲明其實就是雙方對應(yīng)關(guān)系的PropertyName, 覺得繞的話, 直接打開CoreData圖形化界面, 照著上面的剪頭填寫就行了, 站在CoreTeam的立場看, 它的關(guān)系屬性名是coach, 而自己在對方的屬性是team, 所以在Team.m里的關(guān)系描述就是@{@"coach" : @"team"}, 同理, Coach.m里面就是@{@"team" : @"coach"}.

因為一對一關(guān)系其實就是兩個CustomObject屬性互相引用, 所以們只需要在HHPropertyTypeCustomObject處加上這個引用關(guān)系即可. 不過和普通的單向CustomObject屬性不同, 互相引用的屬性在解析時需要注意一下循環(huán)解析的情況. 仍以Team-Coach關(guān)系舉例: 解析CoreTeam時會順帶解析CoreCoach, 而解析CoreCoach時又會去解析CoreTeam... 這就循環(huán)解析了, 所以我們需要在第二層解析處破除一下這個循環(huán).
另外, 因為一對一關(guān)系的兩個對象實際上也就是循環(huán)引用, 會有內(nèi)存泄漏, 直接使用NSManagedObject時我們不需要關(guān)心這個泄漏, 因為它本身Context中不釋放的緩存, 一出生就自帶內(nèi)存泄漏了. 但是我們轉(zhuǎn)換出來的Model不能這樣搞, 在用完以后需要進行破環(huán)清理, 像這樣:
[team clearRelationship];\\單個數(shù)據(jù)的關(guān)系清理
[teams clearRelationship];\\數(shù)組數(shù)據(jù)的關(guān)系清理 你不需要自己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的實現(xiàn)很簡單, 直接根據(jù)一對一關(guān)系表將循環(huán)引用的部分置空就行了, 需要注意的是: 這個置空關(guān)系是從被引用的一方清空的, 而不是直接清空當前對象. 什么意思呢, 比如team生成時引用了coach, 直接設(shè)置team.coach.team = nil只能破除team和coach之間的循環(huán)引用, 如果coach本身還有其他的一對一屬性, 那么被釋放的只有team, coach和它自己的循環(huán)引用屬性依然不會釋放, 所以, 我們要從coach端挨個釋放.
五. 實現(xiàn)細節(jié): 一對多關(guān)系
一對多和一對一使用方法差不多, 這里以Team-Players舉例, 代碼如下:
@implementation Team
//設(shè)置數(shù)組元素為Model的屬性對應(yīng)的Model類
+ (NSDictionary *)containerPropertyKeypathsForCoreData {
return @{@"players" : @"Player"};
}
//設(shè)置一對多關(guān)系
+ (NSDictionary *)oneToManyRelationship {
return @{@"players" : @"team"};
}
@end
@implementation Player
//設(shè)置一對一關(guān)系
+ (NSDictionary *)oneToOneRelationship {
return @{@"team" : @"players"};
}
@end
//一對多關(guān)系解析
+ (instancetype)objectWithManagedObject:(NSManagedObject *)managedObject ignoreProperties:(NSSet *)ignoreProperties cacheTable:(NSMutableDictionary *)cacheTable {
if (managedObject == nil) { return nil; }
//此時的managedObject是CoreTeam 轉(zhuǎn)換出的object是team
//managedObject.players是CorePlayer數(shù)組 也就是下面的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) {
//...其他屬性 略
//一對多關(guān)系解析
case HHPropertyTypeArray: {
//1. 從容器屬性中 取出的容器元素Model對應(yīng)的類名
//也就是team.players<Player *>
id objectsClass = NSClassFromString(containerPropertyKeypaths[property->_name]);
if (!objectsClass || [ignoreProperties containsObject:property->_name]) { break; }
//2. 從 一對多關(guān)系表 中取出對應(yīng)的屬性名
NSMutableSet *ignorePropertyNames = [NSMutableSet setWithSet:ignoreProperties];
NSString *oneToManyTargetName = oneToManyRelationship[property->_name];
!oneToManyTargetName ?: [ignorePropertyNames addObject:oneToManyTargetName];
//3. forin解析CorePlayer數(shù)組為Player數(shù)組
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數(shù)組)
((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;//只設(shè)置任意一邊的關(guān)系即可
[team save];
[team clearRelationship];//不用的時候記得清理
和一對一關(guān)系一樣, 我們需要在各自的.m聲明相應(yīng)的關(guān)系, 站在team的角度看, 它和Player的關(guān)系是一對多的(一個隊伍有多個隊員), 所以在Team.m一對多(oneToManyRelationship)填上@{@"players" : @"team"}, 但站在player的角度看, 它和Team的關(guān)系是一對一(但一個隊員只屬于一只隊伍)的, 所以在Player.m一對一(oneToOneRelationship)填上@{@"team" : @"players"}.

一對多的解析在Team這一方很簡單, 只是簡單的把CorePlayer數(shù)組轉(zhuǎn)換成Player數(shù)組即可, 但是對Player這一方卻需要加上一些小小的改動, 因為Player對Team是一對一的, 所以我們從數(shù)據(jù)庫取出無論多少個CorePlayer這些CorePlayer對應(yīng)的CoreTeam都應(yīng)該是同一個, 也就是說我們不需要針對每個被解析的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是一對多
//所以在設(shè)置關(guān)系是不能直接設(shè)置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.從 一對一關(guān)系表 中取出對應(yīng)的關(guān)系表
NSString *oneToOneTargetName = oneToOneRelationship[property->_name];
//2.以NSManagedObject的地址判斷重用表中是否有可重用數(shù)據(jù)
NSString *cachedObjectKey = [NSString stringWithFormat:@"%p", propertyValue];
if ([cacheTable.allKeys containsObject:cachedObjectKey]) {
propertyValue = cacheTable[cachedObjectKey];
} else {
//3.沒有可重用數(shù)據(jù) 進入解析流程 并將解析結(jié)果放入重用表
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.如果關(guān)系的另一方也是一對一關(guān)系直接設(shè)置即可
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.如果關(guān)系的另一方是一對多關(guān)系需要設(shè)置自己到它的數(shù)組中
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;
}
六. 實現(xiàn)細節(jié): 多對多關(guān)系
多對多關(guān)系其實就是兩個一對多的組合, 直接在HHPropertyTypeArray的代碼基礎(chǔ)上稍作修改即可:
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;
七.待優(yōu)化
個人比較懶散, 目前我只實現(xiàn)了一些自己用得上的基本功能, 還有很多可以優(yōu)化的點, 比如:
1.目前對所有的數(shù)據(jù)操作都是共用的一個隊列, 其實可以建立一個隊列池針對性的進行數(shù)據(jù)同步.
2.HHPredicate目前只定義了==和in關(guān)系, 其實完全可以加上大于, 小于, like%...等等的操作, 到時候?qū)懗蒑asonry那樣的鏈式調(diào)用或者NSMutableAttributedString那樣的無限AddAttribute也行.
3.CoreData默認是懶加載的, 對于那些擁有一對多關(guān)系的類, 我們不需要在一開始就將所有的屬性數(shù)組都從加載出來, 選擇性的加載即可.
...
寫在最后
感覺寫了好多字啊, 其實核心代碼只有300行左右...
最后說一點吧, 因為這套工具是 對象映射+CoreData操作, 可能有些朋友會擔(dān)心有效率問題, 其實不用擔(dān)心, 對象映射的效率在之前的文章我有提過, 很快! 最耗時的其實是CoreData本身的存儲操作, 但這部分顯然是無法優(yōu)化的(可能以后會變好). 所以, 如果你接受不了原生的存儲速度的話, 你應(yīng)該放棄CoreData, 擁抱SQLite/Realm...
本文附帶的demo地址