讓CoreData更簡單些

前言

本文并不是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"}.

屏幕快照 2017-04-09 下午4.21.51.png

因為一對一關(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"}.

屏幕快照 2017-04-09 下午5.26.58.png

一對多的解析在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地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容