coreData詳解

一、初識CoreData

CoreData的結構構成:ios

NSManagedObjectModel的構成:git

  能夠經過Entity建立繼承自NSManagedObject類的文件,這個文件就是開發中使用的託管對象,具有模型對象的表示功能,CoreData的本地持久化都是經過這個類及其子類完成的。github

  在CoreData的總體結構中,主要分爲兩部分。一個是NSManagedObjectContext管理的模型部分,管理着全部CoreData的託管對象。一個是SQLite實現的本地持久化部分,負責和SQL數據庫進行數據交互,主要由NSPersistentStore類操做。這就構成了CoreData的大致結構。正則表達式


  從圖中能夠看出,這兩部分都是比較獨立的,兩部分的交互由一個持久化存儲調度器(NSPersistentStoreCoordinator)來控制。上層NSManagedObjectContext存儲的數據都是交給持久化調度器,由調度器調用具體的持久化存儲對象(NSPersistentStore)來操做對應的數據庫文件,NSPersistentStore負責存儲的實現細節。這樣就很好的將兩部分實現了分離。算法

 

二、認識CoreData-基礎使用

  在模型文件的實體中,參數類型和平時建立繼承自NSObject的模型類大致相似,可是仍是有一些關於類型的說明,下面簡單的列舉了一下。sql

  • Undefined: 默認值,參與編譯會報錯數據庫

  • Integer 16: 整數,表示範圍 -32768 ~ 32767express

  • Integer 32: 整數,表示範圍 -2147483648 ~ 2147483647數組

  • Integer 64: 整數,表示範圍 –9223372036854775808 ~ 9223372036854775807緩存

  • Float: 小數,經過MAXFLOAT宏定義來看,最大值用科學計數法表示是 0x1.fffffep+127f

  • Double: 小數,小數位比Float更精確,表示範圍更大

  • String: 字符串,用NSString表示

  • Boolean: 布爾值,用NSNumber表示

  • Date: 時間,用NSDate表示

  • Binary Data: 二進制,用NSData表示

  • Transformable: OC對象,用id表示。能夠在建立託管對象類文件後,手動改成對應的OC類名。使用的前提是,這個OC對象必須遵照並實現NSCoding協議

 

  在實體最下面,有一個Fetched Properties選項,這個選項用的很少,這裏就不細講了。Fetched Properties用於定義查詢操做,和NSFetchRequest功能相同。定義fetchedProperty對象後,能夠經過NSManagedObjectModel類的fetchRequestFromTemplateWithName:substitutionVariables:方法或其餘相關方法獲取這個fetchedProperty對象。

獲取這個對象後,系統會默認將這個對象緩存到一個字典中,緩存以後也能夠經過fetchedProperty字典獲取fetchedProperty對象。

屬性設置:

  • default Value: 設置默認值,除了二進制不能設置,其餘類型幾乎都能設置。

  • optional: 在使用時是否可選,也能夠理解爲若是設置爲NO,只要向MOC進行save操做,這個屬性是否必須有值。不然MOC進行操做時會失敗並返回一個error,該選項默認爲YES

  • transient: 設置當前屬性是否只存在於內存,不被持久化到本地,若是設置爲YES,這個屬性就不參與持久化操做,屬性的其餘操做沒有區別。transient很是適合存儲一些在內存中緩存的數據,例如存儲臨時數據,這些數據每次都是不一樣的,並且不須要進行本地持久化,因此能夠聲明爲transient的屬性。

  • indexed: 設置當前屬性是不是索引。添加索引後能夠有效的提高檢索操做的速度。可是對於刪除這樣的操做,刪除索引後其餘地方還須要作出相應的變化,因此速度會比較慢。

  • Validation: 經過Validation能夠設置Max ValueMin Value,經過這兩個條件來約定數據,對數據的存儲進行一個驗證。數值類型都有相同的約定方式,而字符串則是約定長度,date是約定時間。

  • Reg. Ex.(Regular Expression): 能夠設置正則表達式,用來驗證和控制數據,不對數據自身產生影響。(只能應用於String類型)

  • Allows External Storage: 當存儲二進制文件時,若是遇到比較大的文件,是否存儲在存儲區以外。若是選擇YES,存儲文件大小超過1MB的文件,都會存儲在存儲區以外。不然大型文件存儲在存儲區內,會形成SQLite進行表操做時,效率受到影響。

Relationships設置:

  • delete rule: 定義關聯屬性的刪除規則。在當前對象和其餘對象有關聯關係時,當前對象被刪除後與之關聯對象的反應。這個參數有四個枚舉值,代碼對應着模型文件的相同選項。

    NSNoActionDeleteRule 刪除後沒有任何操做,也不會將關聯對象的關聯屬性指向nil。刪除後使用關聯對象的關聯屬性,可能會致使其餘問題。

    NSNullifyDeleteRule 刪除後會將關聯對象的關聯屬性指向nil,這是默認值。

    NSCascadeDeleteRule 刪除當前對象後,會將與之關聯的對象也一併刪除。

    NSDenyDeleteRule 在刪除當前對象時,若是當前對象還指向其餘關聯對象,則當前對象不能被刪除。

  • Type: 主要有兩種類型,To OneTo Many,表示當前關係是一對多仍是一對一。

實體:

  • Parent Entity: 能夠在實體中建立繼承關係,在一個實體的菜單欄中經過Parent Entity能夠設置父實體,這樣就存在了實體的繼承關係,最後建立出來的託管模型類也是具備繼承關係的。注意繼承關係中屬性名不要相同。

  使用了這樣的繼承關係後,系統會將子類繼承父類的數據,存在父類的表中,全部繼承自同一父類的子類都會將父類部分存放在父類的表中。這樣可能會致使父類的表中數據量過多,形成性能問題。


二、CoreData-基礎使用

Fetched Properties

  在實體最下面,有一個Fetched Properties選項,這個選項用的很少,這裏就不細講了。Fetched Properties用於定義查詢操做,和NSFetchRequest功能相同。定義fetchedProperty對象後,能夠經過NSManagedObjectModel類的fetchRequestFromTemplateWithName:substitutionVariables:方法或其餘相關方法獲取這個fetchedProperty對象。

獲取這個對象後,系統會默認將這個對象緩存到一個字典中,緩存以後也能夠經過fetchedProperty字典獲取fetchedProperty對象。

Fetch Requests

  在模型文件中Entities下面有一個Fetch Requests,這個也是配置請求對象的。可是這個使用起來更加直觀,能夠很容易的完成一些簡單的請求配置。相對於上面講到的Fetched Properties,這個仍是更方便使用一些。



  上面是對Employee實體的height屬性配置的Fetch Request,這裏配置的height小於2米。配置以後能夠經過NSManagedObjectModel類的fetchRequestTemplateForName:方法獲取這個請求對象,參數是這個請求配置的名稱,也就是EmployeeFR

CoreData增刪改查

- (IBAction)SchoolAdd:(UIButton *)sender {
    // 建立託管對象,並指明建立的託管對象所屬實體名
    _student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext];
    _student.name = @"lxz";
    // 實體中全部基礎數據類型,建立類文件後默認都是NSNumber類型的
    _student.age = @(23);
    
    // 經過上下文保存對象,並在保存前判斷是否有更改
    NSError * error = nil;
    if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) {
        BOOL isAddSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error];
        if (isAddSuccess) {
            NSLog(@"SchoolisAddSuccess");
        }
    }
    
    // 錯誤處理,能夠在這實現本身的錯誤處理邏輯
    if (error) {
        NSLog(@"CoreData Insert Data Error : %@", error);
    }
}
- (IBAction)SchoolDelete:(UIButton *)sender {
    // 創建獲取數據的請求對象,指明對Student實體進行刪除操做
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 建立謂詞對象,過濾出符合要求的對象,也就是要刪除的對象
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
    // 執行獲取操做,找到要刪除的對象
    NSError * error = nil;
    NSArray<Student *> * students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    // 遍歷符合刪除要求的對象數組,執行刪除操做
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext deleteObject:obj];
    }];
    // 保存上下文,並判斷當前上下文是否有改動
    if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) {
        BOOL isDeleteSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:nil];
        if (isDeleteSuccess) {
            NSLog(@"SchoolisDeleteSuccess");
        }
    }
    // 錯誤處理
    if (error) {
        NSLog(@"CoreData Delete Data Error : %@", error);
    }
}
- (IBAction)SchoolUpdate:(UIButton *)sender {
    // 創建獲取數據的請求對象,並指明操做的實體爲Student
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 建立謂詞對象,設置過濾條件
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
    request.predicate = predicate;
    
    // 執行獲取請求,獲取到符合要求的託管對象
    NSError * error = nil;
    NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    
    // 遍歷獲取到的數組,並執行修改操做
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        obj.age = @(24);
    }];
    
    // 將上面的修改進行存儲
    if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) {
        BOOL isUpdateSuccess = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:nil];
        if (isUpdateSuccess) {
            NSLog(@"SchoolIsUpdateSuccess");
        }
    }
    
    // 錯誤處理
    if (error) {
        NSLog(@"CoreData Update Data Error : %@", error);
    }
    
    /**
     在上面簡單的設置了NSPredicate的過濾條件,對於比較複雜的業務需求,還能夠設置複合過濾條件,例以下面的例子
     [NSPredicate predicateWithFormat:@"(age < 25) AND (firstName = XiaoZhuang)"]
     
     也能夠經過NSCompoundPredicate對象來設置複合過濾條件
     [[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[predicate1, predicate2]]
     */
}
- (IBAction)SchoolSearch:(UIButton *)sender {
    // 創建獲取數據的請求對象,指明操做的實體爲Student
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    
    // 執行獲取操做,獲取全部Student託管對象
    NSError * error = nil;
    NSArray<Student *> * students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    
    // 遍歷輸出查詢結果
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"Student Name : %@, Age : %d", obj.name, obj.age);
    }];
    
    // 錯誤處理
    if (error) {
        NSLog(@"CoreData Ergodic Data Error : %@", error);
    }
}

 

三、CoreData-使用進階

  CoreData中能夠經過設置NSFetchRequest類的predicate屬性,來設置一個NSPredicate類型的謂詞對象當作過濾條件。經過設置這個過濾條件,能夠只獲取符合過濾條件的託管對象,不會將全部託管對象都加載到內存中。這樣是很是節省內存和加快查找速度的,設計一個好的NSPredicate能夠優化CoreData搜索性能。

[NSPredicate predicateWithFormat:@"age >= 30"]

  能夠經過NSPredicateiOS中的集合對象執行過濾操做,能夠是NSArrayNSSet及其子類。對不可變數組NSArray執行的過濾,過濾後會返回一個NSArray類型的結果數組,其中存儲着符合過濾條件的對象。

NSArray *results = [array filteredArrayUsingPredicate:predicate]

謂詞不僅能夠過濾簡單條件,還能夠過濾複雜條件,設置複合過濾條件。

[NSPredicate predicateWithFormat:@"(age < 25) AND (firstName = XiaoZhuang)"]

固然也能夠經過NSCompoundPredicate對象來設置複合過濾條件,返回結果是一個NSPredicate的子類NSCompoundPredicate對象。

[[NSCompoundPredicate alloc] initWithType:NSAndPredicateType subpredicates:@[predicate1, predicate2]]

NSPredicate中還可使用正則表達式,能夠經過正則表達式完成一些複雜需求,這使得謂詞的功能更增強大,例以下面是一個手機號驗證的正則表達式

NSString *mobile = @"^1(3[0-9]|5[0-35-9]|8[025-9])\\d{8}$";
NSPredicate *regexmobile = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", mobile];

NSPredicate支持對數據的模糊查詢,例以下面使用通配符來匹配包含lxz的結果,具體CoreData中的使用在下面會講到。

[NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"]

NSPredicate在建立查詢條件時,還支持設置被匹配目標的keyPath,也就是設置更深層被匹配的目標。例以下面設置employeename屬性爲查找條件,就是用點語法設置的keyPath

[NSPredicate predicateWithFormat:@"employee.name = %@", @"lxz"]

在執行fetch操做前,能夠給NSFetchRequest設置一些參數,這些參數包括謂詞、排序等條件,下面是一些基礎的設置。

  • 設置查找哪一個實體,從數據庫的角度來看就是查找哪張表,經過fetchRequestWithEntityName:或初始化方法來指定表名。
  • 經過NSPredicate類型的屬性,能夠設置查找條件,這個屬性在開發中用得最多。NSPredicate能夠包括固定格式的條件以及正則表達式
  • 經過sortDescriptors屬性,能夠設置獲取結果數組的排序方式,這個屬性是一個數組類型,也就是能夠設置多種排序條件。(可是注意條件不要衝突)
  • 經過fetchOffset屬性設置從查詢結果的第幾個開始獲取,經過fetchLimit屬性設置每次獲取多少個。主要用於分頁查詢,後面會講。

  MOC執行fetch操做後,獲取的結果是以數組的形式存儲的,數組中存儲的就是託管對象。NSFetchRequest提供了參數resultType,參數類型是一個枚舉類型。經過這個參數,能夠設置執行fetch操做後返回的數據類型。

設置獲取條件

// 創建獲取數據的請求對象,並指明操做Employee表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];

// 設置請求條件,經過設置的條件,來過濾出須要的數據
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name = %@", @"lxz"];
request.predicate = predicate;

// 設置請求結果排序方式,能夠設置一個或一組排序方式,最後將全部的排序方式添加到排序數組中
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];
// NSSortDescriptor的操做都是在SQLite層級完成的,不會將對象加載到內存中,因此對內存的消耗是很是小的
request.sortDescriptors = @[sort];

// 執行獲取請求操做,獲取的託管對象將會被存儲在一個數組中並返回
NSError *error = nil;
NSArray<Employee *> *employees = [context executeFetchRequest:request error:&error];
[employees enumerateObjectsUsingBlock:^(Employee * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"Employee Name : %@, Height : %@, Brithday : %@", obj.name, obj.height, obj.brithday);
}];

// 錯誤處理
if (error) {
    NSLog(@"CoreData Fetch Data Error : %@", error);
}

這裏設置NSFetchRequest對象的一些請求條件,設置查找Employee表中namelxz的數據,而且將全部符合的數據用height升序的方式排列。

查詢操做

// 建立獲取數據的請求對象,並指明操做Department表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Department"];

// 設置請求條件,設置employee的name爲請求條件。NSPredicate的好處在於,能夠設置keyPath條件
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"employee.name = %@", @"lxz"];
request.predicate = predicate;

// 執行查找操做
NSError *error = nil;
NSArray<Department *> *departments = [context executeFetchRequest:request error:&error];
[departments enumerateObjectsUsingBlock:^(Department * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"Department Search Result DepartName : %@, employee name : %@", obj.departName, obj.employee.name);
}];

// 錯誤處理
if (error) {
    NSLog(@"Department Search Error : %@", error);
}

查找Department實體,並打印實體內容。就像上面講的雙向關係同樣,有關聯關係的實體,本身被查找出來後,也會將與之關聯的其餘實體也查找出來,而且查找出來的實體都是關聯着MOC的。

分頁查詢

在從本地存儲區獲取數據時,能夠指定從第幾個獲取,以及本次查詢獲取多少個數據,聯合起來使用就是分頁查詢。固然也能夠根據需求,單獨使用這兩個API

這種需求在實際開發中很是常見,例如TableView中,上拉加載數據,每次加載20條數據,就能夠利用分頁查詢輕鬆實現。

#pragma mark - ----- Page && Fuzzy ------
//分頁查詢
- (IBAction)pageSearch:(UIButton *)sender {
    // 建立獲取數據的請求對象,並指明操做Student表
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    
    // 設置查找起始點,這裏是從搜索結果的第六個開始獲取
    request.fetchOffset = 6;
    
    // 設置分頁,每次請求獲取六個託管對象
    request.fetchLimit = 6;
    
    // 設置排序規則,這裏設置年齡升序排序
    NSSortDescriptor * descriptor = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
    request.sortDescriptors = @[descriptor];
    
    // 執行查詢操做
    NSError * error = nil;
    NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    
    // 遍歷輸出查詢結果
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"Page Search Result Name : %@, Age : %d", obj.name, obj.age);
    }];
    
    // 錯誤處理
    if (error) {
        NSLog(@"Page Search Data Error : %@", error);
    }
}

上面是一個按照身高升序排序,分頁獲取搜索結果的例子。查找Employee表中的實體,將結果按照height字段升序排序,並從結果的第六個開始查找,而且設置獲取的數量也是六個。

模糊查詢

有時須要獲取具備某些相同特徵的數據,這樣就須要對查詢的結果作模糊匹配。在CoreData執行模糊匹配時,能夠經過NSPredicate執行這個操做。

//模糊查詢
- (IBAction)fuzzySearch:(UIButton *)sender {
    // 建立獲取數據的請求對象,設置對Student表進行操做
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    
    // 建立模糊查詢條件。這裏設置的帶通配符的查詢,查詢條件是結果包含lxz
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"];
    request.predicate      = predicate;
    
    // 執行查詢操做
    NSError * error = nil;
    NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    
    // 遍歷輸出查詢結果
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"Fuzzy Search Result Name : %@, Age : %d", obj.name, obj.age);
    }];
    
    // 錯誤處理
    if (error) {
        NSLog(@"Fuzzy Search Data Error : %@", error);
    }
    
    /**
     模糊查詢的關鍵在於設置模糊查詢條件,除了上面的模糊查詢條件,還能夠設置下面三種條件
     */
    // 以lxz開頭
    // NSPredicate *predicate1 = [NSPredicate predicateWithFormat:@"name BEGINSWITH %@", @"lxz"];
    // 以lxz結尾
    // NSPredicate *predicate2 = [NSPredicate predicateWithFormat:@"name ENDSWITH %@"  , @"lxz"];
    // 其中包含lxz
    // NSPredicate *predicate3 = [NSPredicate predicateWithFormat:@"name contains %@"  , @"lxz"];
    // 還能夠設置正則表達式做爲查找條件,這樣使查詢條件更增強大,下面只是給了個例子
    // NSString *mobile = @"^1(3[0-9]|5[0-35-9]|8[025-9])\\d{8}$";
    // NSPredicate *predicate4 = [NSPredicate predicateWithFormat:@"SELF MATCHES %@", mobile];
}

上面是使用通配符的方式進行模糊查詢NSPredicate支持多種形式的模糊查詢,下面列舉一些簡單的匹配方式。模糊查詢條件對大小寫不敏感,因此查詢條件大小寫都可。

加載請求模板

在以前的文章中談到在模型文件中設置請求模板,也就是在.xcdatamodeld文件中,設置Fetch Requests,使用時能夠經過對應的NSManagedObjectModel獲取設置好的模板。

#pragma mark - ----- Fetch Request ------
/**
 加載模型文件中設置的FetchRequest請求模板,模板名爲StudentAge,在School.xcdatamodeld中設置
 */
- (IBAction)fetchRequest:(UIButton *)sender {
    // 經過MOC獲取託管對象模型,託管對象模型至關於.xcdatamodeld文件,存儲着.xcdatamodeld文件的結構
    NSManagedObjectModel * model = [CoreDataManager sharedCoreDataManager].persistentContainer.managedObjectModel;
    
    // 經過.xcdatamodeld文件中設置的模板名,獲取請求對象
    NSFetchRequest * fetchRequest = [model fetchRequestTemplateForName:@"StudentAge"];
    
    // 請求數據,下面的操做和普通請求同樣
    NSError *error = nil;
    NSArray<Student *> *dataList = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error];
    
    // 遍歷獲取結果,並打印結果
    [dataList enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"Student.count = %ld, Student.age = %d", dataList.count, obj.age);
    }];
    
    // 錯誤處理
    if (error) {
        NSLog(@"Execute Fetch Request Error : %@", error);
    }
}

請求結果排序

/**
 對請求結果進行排序
 這個排序是發生在數據庫一層的,並非將結果取出後排序,因此效率比較高
 */
- (IBAction)resultSort:(UIButton *)sender {
    // 創建獲取數據的請求對象,並指明操做Student表
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 設置請求條件,經過設置的條件,來過濾出須要的數據
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"];
    request.predicate = predicate;
    // 設置請求結果排序方式,能夠設置一個或一組排序方式,最後將全部的排序方式添加到排序數組中
    NSSortDescriptor * sort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
    // NSSortDescriptor的操做都是在SQLite層級完成的,不會將對象加載到內存中,因此對內存的消耗是很是小的
    // 下面request的sort對象是一個數組,也就是能夠設置多種排序條件,但注意條件不要衝突
    request.sortDescriptors = @[sort];
    // 執行獲取請求操做,獲取的託管對象將會被存儲在一個數組中並返回
    NSError * error = nil;
    NSArray<Student *> *students = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:request error:&error];
    // 遍歷返回結果
    [students enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSLog(@"Employee Name : %@, Age : %d", obj.name, obj.age);
    }];
    // 錯誤處理
    if (error) {
        NSLog(@"CoreData Fetch Data Error : %@", error);
    }
}

獲取結果Count值

開發過程當中有時須要只獲取所需數據的Count值,也就是執行獲取操做後數組中所存儲的對象數量。遇到這個需求,若是像以前同樣MOC執行獲取操做,獲取到數組而後取Count,這樣對內存消耗是很大的

對於這個需求,蘋果提供了兩種經常使用的方式獲取這個Count值。這兩種獲取操做,都是在數據庫中完成的,並不須要將託管對象加載到內存中,對內存的開銷也是很小的。

方法1,設置resultType

/**
 獲取返回結果的Count值,經過設置NSFetchRequest的resultType屬性
 */
- (IBAction)getResultCount1:(UIButton *)sender {
    // 設置過濾條件,能夠根據需求設置本身的過濾條件
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < 24"];
    // 建立請求對象,並指明操做Student表
    NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    fetchRequest.predicate = predicate;
    // 這一步是關鍵。設置返回結果類型爲Count,返回結果爲NSNumber類型
    fetchRequest.resultType = NSCountResultType;
    // 執行查詢操做,返回的結果仍是數組,數組中只存在一個對象,就是計算出的Count值
    NSError * error = nil;
    NSArray * dataList = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error];
    // 返回結果存在數組的第一個元素中,是一個NSNumber的對象,經過這個對象便可得到Count值
    NSInteger count = [dataList.firstObject integerValue];
    NSLog(@"fetch request result Employee.count = %ld", count);
    // 錯誤處理
    if (error) {
        NSLog(@"fetch request result error : %@", error);
    }
}

方法1中設置NSFetchRequest對象的resultTypeNSCountResultType,獲取到結果的Count值。這個枚舉值在以前的文章中提到過,除了Count參數,還能夠設置其餘三種參數。

方法2,使用MOC提供的方法

- (IBAction)getResultCount2:(UIButton *)sender {
    // 設置過濾條件
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < 24"];
    // 建立請求對象,指明操做Student表
    NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    fetchRequest.predicate = predicate;
    // 經過調用MOC的countForFetchRequest:error:方法,獲取請求結果count值,返回結果直接是NSUInteger類型變量
    NSError * error = nil;
    NSUInteger count = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext countForFetchRequest:fetchRequest error:&error];
    NSLog(@"fetch request result count is : %ld", count);
    // 錯誤處理
    if (error) {
        NSLog(@"fetch request result error : %@", error);
    }
}

MOC提供了專門獲取請求結果Count值的方法,經過這個方法能夠直接返回一個NSUInteger類型的Count值,使用起來比上面的方法更方便點,其餘都是同樣的。

位運算

假設有需求是對Employee表中,全部託管對象的height屬性計算總和。這個需求在數據量比較大的狀況下,將全部託管對象加載到內存中是很是消耗內存的,就算批量加載也比較耗時耗內存。

CoreData對於這樣的需求,提供了位運算的功能。MOC在執行請求時,是支持對數據進行位運算的。這個操做依然是在數據庫層完成的,對內存的佔用很是小。

/**
 對返回的結果進行按位運算,這個運算是發生在SQLite數據庫層的,因此執行效率很快,對內存的消耗也很小
 若是須要對託管對象的某個屬性進行運算,比較推薦這種效率高的方法.
 */
- (IBAction)bitwiseArithmetic:(UIButton *)sender {
    // 建立請求對象,指明操做Student表
    NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 設置返回值爲字典類型,這是爲告終果能夠經過設置的name名取出,這一步是必須的
    fetchRequest.resultType = NSDictionaryResultType;
    // 建立描述對象的name字符串
    NSString * descriptionName = @"sumOperatin";
    // 建立描述對象
    NSExpressionDescription * expressionDes = [[NSExpressionDescription alloc] init];
    // 設置描述對象的name,最後結果須要用這個name當作key來取出結果
    expressionDes.name = descriptionName;
    // 設置返回值類型,根據運算結果設置類型
    expressionDes.expressionResultType = NSInteger16AttributeType;
    // 建立具體描述對象,用來描述對哪一個屬性進行什麼運算(可執行的運算類型不少,這裏描述的是對age屬性,作sum運算)
    NSExpression * expression = [NSExpression expressionForFunction:@"sum:" arguments:@[[NSExpression expressionForKeyPath:@"age"]]];
    // 只能對應一個具體描述對象
    expressionDes.expression = expression;
    // 給請求對象設置描述對象,這裏是一個數組類型,也就是能夠設置多個描述對象
    fetchRequest.propertiesToFetch = @[expressionDes];
    // 執行請求,返回值仍是一個數組,數組中只有一個元素,就是存儲計算結果的字典
    NSError * error = nil;
    NSArray * resultArr = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeFetchRequest:fetchRequest error:&error];
    // 經過上面設置的name值,當作請求結果的key取出計算結果
    NSNumber * number = resultArr.firstObject[descriptionName];
    NSLog(@"fetch request result is %ld", [number integerValue]);
    // 錯誤處理
    if (error) {
        NSLog(@"fetch request result error : %@", error);
    }
    
    /**
     位運算支持的算法種類不少,具體能夠在NSExpression.h文件中查看
     */
}

從執行結果能夠看到,MOC對全部查找到的託管對象height屬性執行了求和操做,並將結果放在字典中返回。位運算主要是經過NSFetchRequest對象的propertiesToFetch屬性設置,這個屬性能夠設置多個描述對象,最後經過不一樣的name當作key來取出結果便可。

NSExpression類能夠描述多種運算,能夠在NSExpression.h文件中的註釋部分,看到全部支持的運算類型,大概看了一下有二十多種運算。並且除了上面NSExpression調用的方法,此類還支持點語法的位運算,例以下面的例子。

[NSExpression expressionWithFormat:@"@sum.height"];

批處理

  在使用CoreData以前,我和公司同事也討論過,假設遇到須要大量數據處理的時候怎麼辦。CoreData對於大量數據處理的靈活性確定不如SQLite,這時候還須要本身使用其餘方式優化數據處理。雖然在移動端這種狀況不多出現,可是在持久層設計時仍是要考慮這方面。

  當須要進行數據的處理時,CoreData須要先將數據加載到內存中,而後才能對數據進行處理。這樣對於大量數據來講,都加載到內存中是很是消耗內存的,並且容易致使崩潰的發生。若是遇到更改全部數據的某個字段這樣的簡單需求,須要將相關的託管對象都加載到內存中,而後進行更改、保存。

  對於上面這樣的問題,CoreDataiOS8推出了批量更新API,經過這個API能夠直接在數據庫一層就完成更新操做,而不須要將數據加載到內存。除了批量更新操做,在iOS9中還推出了批量刪除API,也是在數據庫一層完成的操做。關於批處理的API不少都是iOS8iOS9出來的,使用時須要注意版本兼容

  可是有個問題,批量更新和批量刪除的兩個API,都是直接對數據庫進行操做,更新完以後會致使MOC緩存和本地持久化數據不一樣步的問題。因此須要手動刷新受影響的MOC中存儲的託管對象,使MOC和本地統一。假設你使用了NSFetchedResultsController,爲了保證界面和數據的統一,這一步更新操做更須要作。

批量更新

#pragma mark - ----- Batch Operation ------
/**
 注意:不管是批量更新仍是批量刪除,這個批量操做都是發生在SQLite層的。然而在SQLite發生了批量操做後,並不會主動更新上層MOC中緩存的託管對象,因此在進行批量操做後,須要對相關的MOC進行更新操做。
 雖然在客戶端不多遇到大量數據處理的狀況,可是若是遇到這樣的需求,推薦使用批量處理API。
 */

/**
 批量更新
 */
- (IBAction)batchUpdate:(UIButton *)sender {
    // 建立批量更新對象,並指明操做Student表
    NSBatchUpdateRequest * updateRequest = [NSBatchUpdateRequest batchUpdateRequestWithEntityName:@"Student"];
    // 設置返回值類型,默認是什麼都不返回(NSStatusOnlyResultType),這裏設置返回發生改變的對象Count值
    updateRequest.resultType = NSUpdatedObjectsCountResultType;
    // 設置發生改變字段的字典
    updateRequest.propertiesToUpdate = @{@"name" : @"lxz"};
    // 執行請求後,返回值是一個特定的result對象,經過result的屬性獲取返回的結果。
    // MOC的這個API是從iOS8出來的,因此須要注意版本兼容。
    NSError * error = nil;
    NSBatchUpdateResult * result = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:updateRequest error:&error];
    NSLog(@"batch update count is %ld", [result.result integerValue]);
    // 錯誤處理
    if (error) {
        NSLog(@"batch update request result error : %@", error);
    }
    // 更新MOC中的託管對象,使MOC和本地持久化區數據同步
    [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext refreshAllObjects];
}

上面對Employee表中全部的託管對象height值作了批量更新,在更新時經過設置propertiesToUpdate字典來控制更新字段和更新的值,設置格式是字段名 : 新值。經過設置批處理對象的predicate屬性,設置一個謂詞對象來控制受影響的對象

還能夠對多個存儲區(數據庫)作一樣批處理操做,經過設置其父類affectedStores屬性,類型是一個數組,能夠包含受影響的存儲區,多個存儲區的操做對批量刪除一樣適用

MOC在執行請求方法時,發現方法名也不同了,執行的是executeRequest: error:方法,這個方法是從iOS8以後出來的。方法傳入的參數是NSBatchUpdateRequest類,此類並非繼承自NSFetchRequest類,而是直接繼承自NSPersistentStoreRequest,和NSFetchRequest是平級關係。

批量刪除

/**
 批量刪除
 */
- (IBAction)batchDelete:(UIButton *)sender {
    // 建立請求對象,並指明對Student表作操做
    NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 經過謂詞設置過濾條件,設置條件爲age小於20
    NSPredicate * predicate = [NSPredicate predicateWithFormat:@"age < %ld", 20];
    fetchRequest.predicate = predicate;
    // 建立批量刪除請求,並使用上面建立的請求對象當作參數進行初始化
    NSBatchDeleteRequest * deleteRequest = [[NSBatchDeleteRequest alloc] initWithFetchRequest:fetchRequest];
    // 設置請求結果類型,設置爲受影響對象的Count
    deleteRequest.resultType = NSBatchDeleteResultTypeCount;
    // 使用NSBatchDeleteResult對象來接受返回結果,經過id類型的屬性result獲取結果
    NSError * error = nil;
    NSBatchDeleteResult * result = [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:deleteRequest error:&error];
    NSLog(@"batch delete request result count is %ld", [result.result integerValue]);
    // 錯誤處理
    if (error) {
        NSLog(@"batch delete request error : %@", error);
    }
    // 更新MOC中的託管對象,使MOC和本地持久化區數據同步
    [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext refreshAllObjects];
}

大多數狀況下,涉及到託管對象的操做,都須要將其加載到內存中完成。因此使用CoreData時,須要注意內存的使用,不要在內存中存在過多的託管對象。在已經作系統兼容的狀況下,進行大量數據的操做時,應該儘可能使用批處理來完成操做。

須要注意的是,refreshAllObjects是從iOS9出來的,在iOS9以前由於要作版本兼容,因此須要使用refreshObject: mergeChanges:方法更新託管對象。

異步請求

#pragma mark - ----- Asynchronous Request ------
/**
 異步處理
 */
- (IBAction)asyncRequest:(UIButton *)sender {
    // 建立請求對象,並指明操做Student表
    NSFetchRequest * fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"Student"];
    // 建立異步請求對象,並經過一個block進行回調,返回結果是一個NSAsynchronousFetchResult類型參數
    NSAsynchronousFetchRequest * asycFetchRequest = [[NSAsynchronousFetchRequest alloc] initWithFetchRequest:fetchRequest completionBlock:^(NSAsynchronousFetchResult * _Nonnull result) {
        // 經過返回結果的finalResult屬性,獲取結果數組
        [result.finalResult enumerateObjectsUsingBlock:^(Student * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            NSLog(@"fetch request result Student.count = %ld, Student.name = %@", result.finalResult.count, obj.name);
        }];
    }];
    
    // 執行異步請求,和批量處理執行同一個請求方法
    NSError * error = nil;
    [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext executeRequest:asycFetchRequest error:&error];
    // 錯誤處理
    if (error) {
        NSLog(@"fetch request result error : %@", error);
    }
}

上面經過NSAsynchronousFetchRequest對象建立了一個異步請求,並經過block進行回調。若是有多個請求同時發起不須要擔憂線程安全的問題,系統會將全部的異步請求添加到一個操做隊列中,在前一個任務訪問數據庫時,CoreData會將數據庫加鎖,等前面的執行完成纔會繼續執行後面的操做。

NSAsynchronousFetchRequest提供了cancel方法,也就是能夠在請求過程當中,將這個請求取消。還能夠經過一個NSProgress類型的屬性,獲取請求完成進度。NSAsynchronousFetchRequest類從iOS8開始可使用,因此低版本須要作版本兼容。

須要注意的是,執行請求時MOC併發類型不能是NSConfinementConcurrencyType,這個併發類型已經被拋棄,會致使崩潰。

 

四、CoreData-高級用法

NSFetchedResultsController

  在開發過程當中會常常用到UITableView這樣的視圖類,這些視圖類須要本身管理其數據源,包括網絡獲取、本地存儲都須要寫代碼進行管理。

  而在CoreData中提供了NSFetchedResultsController類(fetched results controller,也叫FRC),FRC能夠管理UITableViewUICollectionView的數據源。這個數據源主要指本地持久化的數據,也能夠用這個數據源配合着網絡請求數據一塊兒使用,主要看業務需求了。

  本篇文章會使用UITableView做爲視圖類,配合NSFetchedResultsController進行後面的演示,UICollectionView配合NSFetchedResultsController的使用也是相似,這裏就不都講了。

簡單介紹

  就像上面說到的,NSFetchedResultsController就像是上面兩種視圖的數據管理者同樣。FRC能夠監聽一個MOC的改變,若是MOC執行了託管對象的增刪改操做,就會對本地持久化數據發生改變,FRC就會回調對應的代理方法,回調方法的參數會包括執行操做的類型、操做的值、indexPath等參數。

  實際使用時,經過FRC「綁定」一個MOC,將UITableView嵌入在FRC的執行流程中。在任何地方對這個「綁定」MOC存儲區作修改,都會觸發FRC的回調方法,在FRC的回調方法中嵌入UITableView代碼並作對應修改便可。

  由此能夠看出FRC最大優點就是,始終和本地持久化的數據保持統一。只要本地持久化的數據發生改變,就會觸發FRC的回調方法,從而在回調方法中更新上層數據源和UI。這種方式講的簡單一點,就能夠叫作數據帶動UI



可是須要注意一點,在FRC的初始化中傳入了一個MOC參數,FRC只能監測傳入的MOC發生的改變。假設其餘MOC對同一個存儲區發生了改變,FRC則不能監測到這個變化,不會作出任何反應。

因此使用FRC時,須要注意FRC只能對一個MOC的變化作出反應,因此在CoreData持久化層設計時,儘可能一個存儲區只對應一個MOC,或設置一個負責UIMOC,這在後面多線程部分會詳細講解。

修改模型文件結構

在寫代碼以前,先對以前的模型文件結構作一些修改。



FRC的時候,只須要用到Employee這一張表,其餘表和設置直接忽略。須要在Employee原有字段的基礎上,增長一個String類型的sectionName字段,這個字段就是用來存儲section title的,在下面的文章中將會詳細講到。

初始化FRC

下面例子是比較經常使用的FRC初始化方式,初始化時指定的MOC,還用以前講過的MOC初始化代碼,UITableView初始化代碼這裏也省略了,主要突出FRC的初始化。

#import "ChatViewController.h"
#import "CoreDataManager.h"
#import "User+CoreDataProperties.h"

@interface ChatViewController ()<UITableViewDataSource, UITableViewDelegate,NSFetchedResultsControllerDelegate>
@property (nonatomic, strong) CoreDataManager * manager;
@property (nonatomic, strong) User * user;
@property (nonatomic, strong) UITableView * tableView;
@property (strong, nonatomic) NSFetchedResultsController * fetchedResultController;
@end

@implementation ChatViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _manager = [CoreDataManager sharedCoreDataManager];
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 64, self.view.bounds.size.width, self.view.bounds.size.height-100) style:UITableViewStylePlain];
    [self.view addSubview:_tableView];
    _tableView.delegate = self;
    _tableView.dataSource = self;
}

#pragma mark - ----- UITableView Delegate ------
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return self.fetchedResultController.sections.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return self.fetchedResultController.sections[section].numberOfObjects;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    _user = [self.fetchedResultController objectAtIndexPath:indexPath];
    
    UITableViewCell * cell = [self.tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath];
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"identifier"];
    }
    cell.textLabel.text = _user.username;
    cell.detailTextLabel.text = _user.age;
    return cell;
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return self.fetchedResultController.sections[section].indexTitle;
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // 刪除託管對象
        _user = [self.fetchedResultController objectAtIndexPath:indexPath];
        [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext deleteObject:_user];
        
        // 保存上下文環境,並作錯誤處理
        NSError * error = nil;
        if (![[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error]) {
            NSLog(@"tableView delete cell error : %@", error);
        }
    }
}

#pragma mark - ----- NSFetchedResultsController ------
#pragma mark - ----- 生成測試數據 ------
//插入數據
- (IBAction)CreateTestData:(UIButton *)sender {
    for (int i = 0; i < 3; i++) {
        _user = [NSEntityDescription insertNewObjectForEntityForName:@"User" inManagedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext];
        _user.username = [NSString stringWithFormat:@"username:%d", i];
        _user.age = [NSString stringWithFormat:@"age:%d", i ];
        _user.sectionName = [NSString stringWithFormat:@"sectionName:%d", i];
    }
    
    NSError * error = nil;
    if ([CoreDataManager sharedCoreDataManager].persistentContainer.viewContext.hasChanges) {
        [[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext save:&error];
    }
}

- (IBAction)RefreshTestData:(UIButton *)sender {
    // 建立請求對象,並指明操做User表
    NSFetchRequest * request = [NSFetchRequest fetchRequestWithEntityName:@"User"];
    // 設置排序規則,指明根據age字段升序排序
    NSSortDescriptor * ageSort = [NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES];
    request.sortDescriptors = @[ageSort];
    // 建立NSFetchedResultsController控制器實例,並綁定MOC
    NSError * error = nil;
    self.fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:[CoreDataManager sharedCoreDataManager].persistentContainer.viewContext sectionNameKeyPath:@"sectionName" cacheName:nil];
    // 設置代理,並遵照協議
    self.fetchedResultController.delegate = self;
    // 執行獲取請求,執行後FRC會從持久化存儲區加載數據,其餘地方能夠經過FRC獲取數據
    [self.fetchedResultController performFetch:&error];
    // 錯誤處理
    if (error) {
        NSLog(@"NSFetchedResultsController init error : %@", error);
    }
    
    // 刷新UI
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"identifier"];
    [self.tableView reloadData];
}

#pragma mark - ----- NSFetchedResultsControllerDelegate ------

// Cell數據源發生改變會回調此方法,例如添加新的託管對象等
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath {
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeMove:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate: {
            User * user = [self.fetchedResultController objectAtIndexPath:indexPath];
            
            UITableViewCell * cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.textLabel.text = user.username;
            [self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
        }
            break;
    }
}

// Section數據源發生改變回調此方法,例如修改section title等
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        default:
            break;
    }
}

// 本地數據源發生改變,將要開始回調FRC代理方法。
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}

// 本地數據源發生改變,FRC代理方法回調完成。
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}

- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName {
    return [NSString stringWithFormat:@"sectionName %@", sectionName];
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}

就像cellForRowAtIndexPath:方法中使用的同樣,FRC提供了兩個方法輕鬆轉換indexPathNSManagedObject的對象,在實際開發中這兩個方法很是實用,這也是FRCUITableViewUICollectionView深度融合的表現。

- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)indexPathForObject:(id)object;
Fetched Results Controller Delegate
// Cell數據源發生改變會回調此方法,例如添加新的託管對象等
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath {

    switch (type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeUpdate: {
            UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
            Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
            cell.textLabel.text = emp.name;
        }
            break;
    }
}

// Section數據源發生改變回調此方法,例如修改section title等。
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {

    switch (type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeDelete:
            [tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        default:
            break;
    }
}

// 本地數據源發生改變,將要開始回調FRC代理方法。
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [tableView beginUpdates];
}

// 本地數據源發生改變,FRC代理方法回調完成。
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [tableView endUpdates];
}

// 返回section的title,能夠在這裏對title作進一步處理。這裏修改title後,對應section的indexTitle屬性會被更新。
- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName {
    return [NSString stringWithFormat:@"sectionName %@", sectionName];
}

上面就是當本地持久化數據發生改變後,被回調的FRC代理方法的實現,能夠在對應的實現中完成本身的代碼邏輯。

在上面的章節中講到刪除cell後,本地持久化數據同步的問題。在刪除cell後在tableView代理方法的回調中,調用了MOC的刪除方法,使本地持久化存儲和UI保持同步,並回調到下面的FRC代理方法中,在代理方法中對UI作刪除操做,這樣一套由UI的改變引起的刪除流程就完成了。

目前爲止已經實現了數據和UI雙向同步,即UI發生改變後本地存儲發生改變,本地存儲發生改變後UI也隨之改變。能夠經過下面添加數據的代碼來測試一下,NSFetchedResultsController就講到這裏了。

- (void)addMoreData {
    Employee *employee = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context];
    employee.name = [NSString stringWithFormat:@"lxz 15"];
    employee.height = @(15);
    employee.brithday = [NSDate date];
    employee.sectionName = [NSString stringWithFormat:@"3"];

    NSError *error = nil;
    if (![context save:&error]) {
        NSLog(@"MOC save error : %@", error);
    }
}

版本遷移

CoreData版本遷移的方式有不少,通常都是先在Xcode中,原有模型文件的基礎上,建立一個新版本的模型文件,而後在此基礎上作不一樣方式的版本遷移。

本章節將會講三種不一樣的版本遷移方案,但都不會講太深,都是從使用的角度講起,能夠知足大多數版本遷移的需求。

爲何要版本遷移?

  在已經運行程序並經過模型文件生成數據庫後,再對模型文件進行的修改,若是隻是修改已有實體屬性的默認值、最大最小值、Fetch Request等屬性自身包含的參數時,並不會發生錯誤。若是修改模型文件的結構,或修改屬性名、實體名等,形成模型文件的結構發生改變,這樣再次運行程序就會致使崩潰

  在開發測試過程當中,能夠直接將原有程序卸載就能夠解決這個問題,可是本地以前存儲的數據也會消失。若是是線上程序,就涉及到版本遷移的問題,不然會致使崩潰,並提示以下錯誤:

 

CoreData: error: Illegal attempt to save to a file that was never opened. "This NSPersistentStoreCoordinator has no persistent stores (unknown).  It cannot perform a save operation.". No last error recorded.

 

然而在需求不斷變化的過程當中,後續版本確定會對原有的模型文件進行修改,這時就須要用到版本遷移的技術,下面開始講版本遷移的方案。

建立新版本模型文件

本文中講的幾種版本遷移方案,在遷移以前都須要對原有的模型文件建立新版本。

選中須要作遷移的模型文件 -> 點擊菜單欄Editor -> Add Model Version -> 選擇基於哪一個版本的模型文件(通常都是選擇目前最新的版本),新建模型文件完成。

對於新版本模型文件的命名,我在建立新版本模型文件時,通常會拿當前工程版本號當作後綴,這樣在模型文件版本比較多的時候,就能夠很容易將模型文件版本和工程版本對應起來

 

添加完成後,會發現以前的模型文件會變成一個文件夾,裏面包含着多個模型文件。

在新建的模型文件中,裏面的文件結構和以前的文件結構相同。後續的修改都應該在新的模型文件上,以前的模型文件不要再動了,在修改完模型文件後,記得更新對應的模型類文件

基於新的模型文件,對Employee實體作以下修改,下面的版本遷移也以此爲例。

添加一個String類型的屬性,設置屬性名爲sectionName

此時還應該選中模型文件,設置當前模型文件的版本。這裏選擇將最新版本設置爲剛纔新建的1.1.0版本,模型文件設置工做完成。

Show The File Inspector -> Model Version -> Current 設置爲最新版本。

 

對模型文件的設置已經完成了,接下來系統還要知道咱們想要怎樣遷移數據。在遷移過程當中可能會存在多種可能,蘋果將這個靈活性留給了咱們完成。剩下要作的就是編寫遷移方案以及細節的代碼。

輕量級版本遷移

輕量級版本遷移方案很是簡單,大多數遷移工做都是由系統完成的,只須要告訴系統遷移方式便可。在持久化存儲協調器(PSC)初始化對應的持久化存儲(NSPersistentStore)對象時,設置options參數便可,參數是一個字典。PSC會根據傳入的字典,自動推斷版本遷移的過程。

字典中設置的key
  • NSMigratePersistentStoresAutomaticallyOption設置爲YESCoreData會試着把低版本的持久化存儲區遷移到最新版本的模型文件。
  • NSInferMappingModelAutomaticallyOption設置爲YESCoreData會試着以最爲合理地方式自動推斷出源模型文件的實體中,某個屬性到底對應於目標模型文件實體中的哪個屬性。

版本遷移的設置是在建立MOC時給PSC設置的,爲了使代碼更直觀,下面只給出發生變化部分的代碼,其餘MOC的初始化代碼都不變。

// 設置版本遷移方案
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
                                NSInferMappingModelAutomaticallyOption : @YES};

// 建立持久化存儲協調器,並將遷移方案的字典當作參數傳入
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
修改實體名

假設須要對已存在實體進行更名操做,須要將重命名後的實體Renaming ID,設置爲以前的實體名。下面是Employee實體進行操做。

修改後再使用實體時,應該將實體名設爲最新的實體名,這裏也就是Employee2,並且數據庫中的數據也會遷移到Employee2表中。

Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.9;
[context save:nil];

Mapping Model 遷移方案

輕量級遷移方案只是針對增長和改變實體、屬性這樣的一些簡單操做,假設有更復雜的遷移需求,就應該使用Xcode提供的遷移模板(Mapping Model)。經過Xcode建立一個後綴爲.xcmappingmodel的文件,這個文件是專門用來進行數據遷移用的,一些變化關係也會體如今模板中,看起來很是直觀

這裏還以上面更改實體名,並遷移實體數據爲例子,將Employee實體遷移到Employee2中。首先將Employee實體更名爲Employee2,而後建立Mapping Model文件。

Command + N 新建文件 -> 選擇 Mapping Model -> 選擇源文件 Source Model -> 選擇目標文件 Target Model -> 命名 Mapping Model 文件名 -> Create 建立完成。

  如今就建立好一個Mapping Model文件,文件中顯示了實體、屬性、Relationships,源文件和目標文件之間的關係。實體命名是EntityToEntity的方式命名的,實體包含的屬性和關聯關係,都會被添加到遷移方案中(Entity MappingAttribute MappingRelationship Mapping)。

在遷移文件的下方是源文件和目標文件的關係。



  在上面圖中更名後的Employee2實體並無遷移關係,因爲是更名後的實體,系統還不知道實體應該怎樣作遷移。因此選中Mapping Model文件的Employee2 Mappings,能夠看到右側邊欄的Sourceinvalid value。由於要從Employee實體遷移數據過來,因此將其選擇爲Employee,遷移關係就設置完成了。

  設置完成後,還應該將以前EmployeeToEmployeeMappings刪除,由於這個實體已經被Employee2替代,它的Mappings也被Employee2 Mappings所替代,不然會報錯。



  在實體的遷移過程當中,還能夠經過設置Predicate的方式,來簡單的控制遷移過程。例如只須要遷移一部分指定的數據,就能夠經過Predicate來指定。能夠直接在右側Filter Predicate的位置設置過濾條件,格式是$source.height < 100$source表明數據源的實體。

更復雜的遷移需求

  若是還存在更復雜的遷移需求,並且上面的遷移方式不能知足,能夠考慮更復雜的遷移方式。假設要在遷移過程當中,對遷移的數據進行更改,這時候上面的遷移方案就不能知足需求了。

  對於上面提到的問題,在Mapping Model文件中選中實體,能夠看到Custom Policy這個選項,選項對應的是NSEntityMigrationPolicy的子類,能夠建立並設置一個子類,並重寫這個類的方法來控制遷移過程。

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;

版本遷移總結

版本遷移在需求的變動中確定是要發生的,可是咱們應該儘可能避免這樣的狀況發生。在最開始設計模型文件數據結構的時候,就應該設計一個比較完善而且容易應對變化的結構,這樣後面就算髮生變化也不會對結構主體形成大的改動。



 

五、CoreData-多線程

CoreData的多線程,其中會包括併發隊列類型、線程安全等技術點。

MOC併發隊列類型

  在CoreDataMOC是支持多線程的,能夠在建立MOC對象時,指定其併發隊列的類型。當指定隊列類型後,系統會將操做都放在指定的隊列中執行,若是指定的是私有隊列,系統會建立一個新的隊列。但這都是系統內部的行爲,咱們並不能獲取這個隊列,隊列由系統所擁有,並由系統將任務派發到這個隊列中執行的

NSManagedObjectContext併發隊列類型:
  • NSConfinementConcurrencyType : 若是使用init方法初始化上下文,默認就是這個併發類型。這個枚舉值是不支持多線程的,從名字上也體現出來了。
  • NSPrivateQueueConcurrencyType : 私有併發隊列類型,操做都是在子線程中完成的。
  • NSMainQueueConcurrencyType : 主併發隊列類型,若是涉及到UI相關的操做,應該考慮使用這個枚舉值初始化上下文。

其中NSConfinementConcurrencyType類型在iOS9以後已經被蘋果廢棄,不建議使用這個API。使用此類型建立的MOC,調用某些比較新的CoreDataAPI可能會致使崩潰。

MOC多線程調用方式

  在CoreDataMOC不是線程安全的,在多線程狀況下使用MOC時,不能簡單的將MOC從一個線程中傳遞到另外一個線程中使用,這並非CoreData的多線程,並且會出問題。對於MOC多線程的使用,蘋果給出了本身的解決方案。

  在建立的MOC中使用多線程,不管是私有隊列仍是主隊列,都應該採用下面兩種多線程的使用方式,而不是本身手動建立線程。調用下面方法後,系統內部會將任務派發到不一樣的隊列中執行。能夠在不一樣的線程中調用MOC的這兩個方法,這個是容許的。

- (void)performBlock:(void (^)())block            異步執行的block,調用以後會馬上返回。
- (void)performBlockAndWait:(void (^)())block     同步執行的block,調用以後會等待這個任務完成,纔會繼續向下執行。

下面是多線程調用的示例代碼,在多線程的環境下執行MOCsave方法,就是將save方法放在MOCblock體中異步執行,其餘方法的調用也是同樣的。

[context performBlock:^{
    [context save:nil];
}];

可是須要注意的是,這兩個block方法不能在NSConfinementConcurrencyType類型的MOC下調用,這個類型的MOC是不支持多線程的,只支持其餘兩種併發方式的MOC

多線程的使用

在業務比較複雜的狀況下,須要進行大量數據處理,而且還須要涉及到UI的操做。對於這種複雜需求,若是都放在主隊列中,對性能和界面流暢度都會有很大的影響,致使用戶體驗很是差,下降屏幕FPS。對於這種狀況,能夠採起多個MOC配合的方式。

CoreData多線程的發展中,在iOS5經歷了一次比較大的變化,以後能夠更方便的使用多線程。從iOS5開始,支持設置MOCparentContext屬性,經過這個屬性能夠設置MOC父MOC。下面會針對iOS5以前和以後,分別講解CoreData的多線程使用。

儘管如今的開發中早就不兼容iOS5以前的系統了,可是做爲了解這裏仍是要講一下,並且這種同步方式在iOS5以後也是能夠正常使用的,也有不少人還在使用這種同步方式,下面其餘章節也是同理。

iOS5以前使用多個MOC

iOS5以前實現MOC的多線程,能夠建立多個MOC,多個MOC使用同一個PSC,並讓多個MOC實現數據同步。經過這種方式不用擔憂PSC在調用過程當中的線程問題,MOC在使用PSC進行save操做時,會對PSC進行加鎖,等當前加鎖的MOC執行完操做以後,其餘MOC才能繼續執行操做。

每個PSC都對應着一個持久化存儲區,PSC知道存儲區中數據存儲的數據結構,而MOC須要使用這個PSC進行save操做的實現。



  這樣作有一個問題,當一個MOC發生改變並持久化到本地時,系統並不會將其餘MOC緩存在內存中的NSManagedObject對象改變。因此這就須要咱們在MOC發生改變時,將其餘MOC數據更新。

  根據上面的解釋,在下面例子中建立了一個主隊列的mainMOC,主要用於UI操做。一個私有隊列的backgroundMOC,用於除UI以外的耗時操做,兩個MOC使用的同一個PSC

簡單狀況下的數據同步

簡單狀況下的數據同步,是針對於只有一個MOC的數據發生改變,並提交存儲區後,其餘MOC更新時並無對相同的數據作改變,只是單純的同步數據的狀況。

NSManagedObjectContext類中,根據不一樣操做定義了一些通知。在一個MOC發生改變時,其餘地方能夠經過MOC中定義的通知名,來獲取MOC發生的改變。在NSManagedObjectContext中定義了下面三個通知:

  • NSManagedObjectContextWillSaveNotification MOC將要向存儲區存儲數據時,調用這個通知。在這個通知中不能獲取發生改變相關的NSManagedObject對象。
  • NSManagedObjectContextDidSaveNotification MOC向存儲區存儲數據後,調用這個通知。在這個通知中能夠獲取改變、添加、刪除等信息,以及相關聯的NSManagedObject對象。
  • NSManagedObjectContextObjectsDidChangeNotificationMOC中任何一個託管對象發生改變時,調用這個通知。例如修改託管對象的屬性。

經過監聽NSManagedObjectContextDidSaveNotification通知,獲取全部MOCsave操做。

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(settingsContext:) name:NSManagedObjectContextDidSaveNotification object:nil];

不須要在通知的回調方法中,編寫代碼對比被修改的託管對象。MOC爲咱們提供了下面的方法,只須要將通知對象傳入,系統會自動同步數據

- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification;

下面是通知中的實現代碼,可是須要注意的是,因爲通知是同步執行的,在通知對應的回調方法中所處的線程,和發出通知的MOC執行操做時所處的線程是同一個線程,也就是系統performBlock:回調方法分配的線程。

因此其餘MOC在通知回調方法中,須要注意使用performBlock:方法,並在block體中執行操做。

- (void)settingsContext:(NSNotification *)noti {
    [context performBlock:^{
        // 調用須要同步的MOC對象的merge方法,直接將通知對象當作參數傳進去便可,系統會完成同步操做。
        [context mergeChangesFromContextDidSaveNotification:noti];
    }];
}

複雜狀況下的數據同步

在一個MOC對本地存儲區的數據發生改變,而其餘MOC也對一樣的數據作了改變,這樣後面執行save操做的MOC就會衝突,並致使後面的save操做失敗,這就是複雜狀況下的數據合併。

這是由於每次一個MOC執行一次fetch操做後,會保存一個本地持久化存儲的狀態,當下次執行save操做時會對比這個狀態和本地持久化狀態是否同樣。若是同樣,則表明本地沒有其餘MOC對存儲發生過改變;若是不同,則表明本地持久化存儲被其餘MOC改變過,這就是形成衝突的根本緣由。

對於這種衝突的狀況,能夠經過MOC對象指定解決衝突的方案,經過mergePolicy屬性來設置方案。mergePolicy屬性有下面幾種可選的策略,默認是NSErrorMergePolicy方式,這也是惟一一個有NSError返回值的選項。

  • NSErrorMergePolicy : 默認值,當出現合併衝突時,返回一個NSError對象來描述錯誤,而MOC和持久化存儲區不發生改變
  • NSMergeByPropertyStoreTrumpMergePolicy : 以本地存儲爲準,使用本地存儲來覆蓋衝突部分。
  • NSMergeByPropertyObjectTrumpMergePolicy : 以MOC的爲準,使用MOC來覆蓋本地存儲的衝突部分。
  • NSOverwriteMergePolicy : 以MOC爲準,用MOC的全部NSManagedObject對象覆蓋本地存儲的對應對象。
  • NSRollbackMergePolicy : 以本地存儲爲準,MOC全部的NSManagedObject對象被本地存儲的對應對象所覆蓋。

上面五種策略中,除了第一個NSErrorMergePolicy的策略,其餘四種中NSMergeByPropertyStoreTrumpMergePolicyNSRollbackMergePolicy,以及NSMergeByPropertyObjectTrumpMergePolicyNSOverwriteMergePolicy看起來是重複的。

其實它們並非衝突的,這四種策略的不一樣體如今,對沒有發生衝突的部分應該怎麼處理NSMergeByPropertyStoreTrumpMergePolicyNSMergeByPropertyObjectTrumpMergePolicy對沒有衝突的部分,未衝突部分數據並不會受到影響。而NSRollbackMergePolicyNSOverwriteMergePolicy則是不管是否衝突,直接所有替換。

題外話:
對於MOC的這種合併策略來看,有木有感受到CoreData解決衝突的方式,和SVN解決衝突的方式特別像。。。

線程安全

不管是MOC仍是託管對象,都不該該在其餘MOC的線程中執行操做,這兩個API都不是線程安全的。但MOC能夠在其餘MOC線程中調用performBlock:方法,切換到本身的線程執行操做。

若是其餘MOC想要拿到託管對象,並在本身的隊列中使用託管對象,這是不容許的,託管對象是不能直接傳遞到其餘MOC的線程的。可是能夠經過獲取NSManagedObjectNSManagedObjectID對象,在其餘MOC中經過NSManagedObjectID對象,從持久化存儲區中獲取NSManagedObject對象,這樣就是容許的。NSManagedObjectID是線程安全,而且能夠跨線程使用的。

能夠經過MOC獲取NSManagedObjectID對應的NSManagedObject對象,例以下面幾個MOCAPI

NSManagedObject *object = [context objectRegisteredForID:objectID];
NSManagedObject *object = [context objectWithID:objectID];

經過NSManagedObject對象的objectID屬性,獲取NSManagedObjectID類型的objectID對象。

NSManagedObjectID *objectID = object.objectID;

CoreData多線程結構設計

上面章節中寫的大多都是怎麼用CoreData多線程,在掌握多線程的使用後,就能夠根據公司業務需求,設計一套CoreData多線程結構了。對於多線程結構的設計,應該本着儘可能減小主線程壓力的角度去設計,將全部耗時操做都放在子線程中執行。

對於具體的設計我根據不一樣的業務需求,給出兩種設計方案的建議。

兩層設計方案

在項目中多線程操做比較簡單時,能夠建立一個主隊列mainMOC,和一個或多個私有隊列的backgroundMOC。將全部backgroundMOCparentContext設置爲mainMOC,採起這樣的兩層設計通常就可以知足大多數需求了。



將耗時操做都放在backgroundMOC中執行,mainMOC負責全部和UI相關的操做。全部和UI無關的工做都交給backgroundMOC,在backgroundMOC對數據發生改變後,調用save方法會將改變pushmainMOC中,再由mainMOC執行save方法將改變保存到存儲區。

代碼這裏就不寫了,和上面例子中設置parentContext代碼同樣,主要講一下設計思路。

三層設計方案

可是咱們發現,上面的save操做最後仍是由mainMOC去執行的,backgroundMOC只是負責處理數據。雖然mainMOC只執行save操做並不會很耗時,可是若是save涉及的數據比較多,這樣仍是會對性能形成影響的。

雖然客戶端不多涉及到大量數據處理的需求,可是假設有這樣的需求。能夠考慮在兩層結構之上,給mainMOC之上再添加一個parentMOC,這個parentMOC也是私有隊列的MOC,用於處理save操做。



這樣CoreData存儲的結構就是三層了,最底層是backgroundMOC負責處理數據,中間層是mainMOC負責UI相關操做,最上層也是一個backgroundMOC負責執行save操做。這樣就將影響UI的全部耗時操做全都剝離到私有隊列中執行,使性能達到了很好的優化。

須要注意的是,執行MOC相關操做時,不要阻塞當前主線程。全部MOC的操做應該是異步的,不管是子線程仍是主線程,儘可能少的使用同步block方法。

MOC同步時機

設置MOCparentContext屬性以後,parent對於child的改變是知道的,可是child對於parent的改變是不知道的。蘋果這樣設計,應該是爲了更好的數據同步。

Employee *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:backgroundMOC];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.7f;

[backgroundMOC performBlock:^{
    [backgroundMOC save:nil];
    [mainMOC performBlock:^{
        [mainMOC save:nil];
    }];
}];

在上面這段代碼中,mainMOCbackgroundMOCparentContext。在backgroundMOC執行save方法前,backgroundMOCmainMOC都不能獲取到Employee的數據,在backgroundMOC執行完save方法後,自身上下文發生改變的同時,也將改變pushmainMOC中,mainMOC也具備了Employee對象。

因此在backgroundMOCsave方法執行時,是對內存中的上下文作了改變,當擁有PSCmainMOC執行save方法後,是對本地存儲區作了改變。

 

六、CoreData-MagicalRecord

CoreData是蘋果自家推出的一個持久化框架,使用起來更加面向對象。可是在使用過程當中會出現大量代碼,並且CoreData學習曲線比較陡峭,若是掌握很差,在使用過程當中很容易形成其餘問題。

國外開發者開源了一個基於CoreData封裝的第三方——MagicalRecord,就像是FMDB封裝SQLite同樣,MagicalRecord封裝的CoreData,使得原生的CoreData更加容易使用。而且MagicalRecord下降了CoreData的使用門檻,不用去手動管理以前的PSCMOC等對象。

根據GithubMagicalRecord的官方文檔,MagicalRecord的優勢主要有三條:

1. 清理項目中CoreData代碼
2. 支持清晰、簡單、一行式的查詢操做
3. 當須要優化請求時,能夠獲取NSFetchRequest進行修改

添加MagicalRecord到項目中

MagicalRecord添加到項目中,和使用其餘第三方同樣,能夠經過下載源碼和CocoaPods兩種方式添加。

1.Github下載MagicalRecord源碼,將源碼直接拖到項目中,後續須要手動更新源碼。

2. 也能夠經過CocoaPods安裝MagicalRecord,須要在Podfile中加入下面命令,後續只須要經過命令來更新。

pod "MagicalRecord"

在以前建立新項目時,經過勾選"Use Core Data"的方式添加CoreData到項目中,會在AppDelegate文件中生成大量CoreData相關代碼。若是是大型項目,被佔用的位置是很重要的。而對於MagicalRecord來講,只須要兩行代碼便可。

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
     // 初始化CoreData堆棧,也能夠指定初始化某個CoreData堆棧
     [MagicalRecord setupCoreDataStack];
     return YES;
 }

 - (void)applicationWillTerminate:(UIApplication *)application {
     // 在應用退出時,應該調用cleanUp方法
     [MagicalRecord cleanUp];
 }

MagicalRecord是支持CoreData.xcdatamodeld文件的,使得CoreData這一優勢能夠繼續使用。創建數據結構時仍是像以前使用CoreData同樣,經過.xcdatamodeld文件的方式創建。

支持iCloud

CoreData是支持iCloud的,MagicalRecordiCloud相關的操做也作了封裝,只須要使用MagicalRecord+iCloud.h類中提供的方法,就能夠進行iCloud相關的操做。

例以下面是MagicalRecord+iCloud.h中的一個方法,須要將相關參數傳入便可。

 + (void)setupCoreDataStackWithiCloudContainer:(NSString *)containerID localStoreNamed:(NSString *)localStore;

建立上下文

MagicalRecord對上下文的管理和建立也比較全面,下面是MagicalRecord提供的部分建立和獲取上下文的代碼。由於是給NSManagedObjectContext添加的Category,能夠直接用NSManagedObjectContext類調用,使用很是方便。

可是須要注意,雖然系統幫咱們管理了上下文對象,對於耗時操做仍然要放在後臺線程中處理,而且在主線程中進行UI操做

+ [NSManagedObjectContext MR_context]  設置默認的上下文爲它的父級上下文,併發類型爲NSPrivateQueueConcurrencyType
+ [NSManagedObjectContext MR_newMainQueueContext]  建立一個新的上下文,併發類型爲NSMainQueueConcurrencyType
+ [NSManagedObjectContext MR_newPrivateQueueContext]  建立一個新的上下文,併發類型爲NSPrivateQueueConcurrencyType
+ [NSManagedObjectContext MR_contextWithParent:]  建立一個新的上下文,容許自定義父級上下文,併發類型爲NSPrivateQueueConcurrencyType
+ [NSManagedObjectContext MR_contextWithStoreCoordinator:]  建立一個新的上下文,並容許自定義持久化存儲協調器,併發類型爲NSPrivateQueueConcurrencyType
+ [NSManagedObjectContext MR_defaultContext]  獲取默認上下文對象,項目中最基礎的上下文對象,併發類型是NSMainQueueConcurrencyType

增刪改查

MagicalRecordNSManagedObject添加了一個Category,將增刪改查等操做放在這個Category中,使得這些操做能夠直接被NSManagedObject類及其子類調用。

.1. 增
對於託管模型的建立很是簡單,不須要像以前還須要進行上下文的操做,如今這都是MagicalRecord幫咱們完成的。

// 建立並插入到上下文中
 Employee *emp = [Employee MR_createEntity];

.2. 刪

 // 從上下文中刪除當前對象
 [emp MR_deleteEntity];

.3. 改

 // 獲取一個上下文對象
 NSManagedObjectContext *defaultContext = [NSManagedObjectContext MR_defaultContext];

 // 在當前上下文環境中建立一個新的Employee對象
 Employee *emp = [Employee MR_createEntityInContext:defaultContext];
 emp.name      = @"lxz";
 emp.brithday  = [NSDate date];
 emp.height    = @1.7;

 // 保存修改到當前上下文中
 [defaultContext MR_saveToPersistentStoreAndWait];

.4. 查

 // 執行查找操做,並設置排序條件
 NSArray *empSorted = [Employee MR_findAllSortedBy:@"height" ascending:YES];

自定義NSFetchRequest

下面示例代碼中,Employee根據已有的employeeFilter謂詞對象,建立了employeeRequest請求對象,並將請求對象作修改後,從MOC中獲取請求結果,實現自定義查找條件。

 NSPredicate *employeeFilter = [NSPredicate predicateWithFormat:@"name LIKE %@", @"*lxz*"];
 NSFetchRequest *employeeRequest = [Employee MR_requestAllWithPredicate:employeeFilter];
 employeeRequest.fetchOffset = 10;
 employeeRequest.fetchLimit = 10;
 NSArray *employees = [Employee MR_executeFetchRequest:employeeRequest];

參數設置

.1. 能夠經過修改MR_LOGGING_DISABLED預編譯指令的值,控制log打印。

#define MR_LOGGING_DISABLED 1

.2.MagicalRecordDEBUG模式下,對模型文件發生了更改,而且沒有建立新的模型文件版本。MagicalRecord默認會將舊的持久化存儲刪除,建立新的持久化存儲。

MagicalRecord中文文檔

MagicalRecord的使用方法還有不少,這裏只是將一些比較經常使用的拿出來說講,其餘就不一一講解了。在Github上有國人翻譯的MagicalRecord官方文檔,翻譯的很是全面,並且是實時更新的。

MagicalRecord中文文檔

 

原文連接:https://github.com/DeveloperErenLiu/BlogDemo-CoreData
相關文章
相關標籤/搜索