認識CoreData - 高級用法

該文章屬於<簡書 — 劉小壯>原創,轉載請註明:

<簡書 — 劉小壯> http://www.jianshu.com/p/01f36026da7dgit


在以前的文章中,已經講了不少關於CoreData使用相關的知識點。這篇文章中主要講兩個方面,NSFetchedResultsController和版本遷移。github

文章題目中雖然有**「高級」**兩個字,其實講的東西並不高級,只是由於上一篇文章中東西太多了,把兩個較複雜的知識點挪到這篇文章中。數據庫

文章中若有疏漏或錯誤,還請各位及時提出,謝謝!數組


佔位圖

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代碼並作對應修改便可。app

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

FRC

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

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

修改模型文件結構

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

Employee結構

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

初始化FRC

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

// 建立請求對象,並指明操做Employee表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 設置排序規則,指明根據height字段升序排序
NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];
request.sortDescriptors = @[heightSort];

// 建立NSFetchedResultsController控制器實例,並綁定MOC
NSError *error = nil;
fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request 
											                  managedObjectContext:context 
											                    sectionNameKeyPath:@"sectionName" 
											                             cacheName:nil];
// 設置代理,並遵照協議
fetchedResultController.delegate = self;
// 執行獲取請求,執行後FRC會從持久化存儲區加載數據,其餘地方能夠經過FRC獲取數據
[fetchedResultController performFetch:&error];

// 錯誤處理
if (error) {
    NSLog(@"NSFetchedResultsController init error : %@", error);
}

// 刷新UI
[tableView reloadData];

在上面初始化FRC時,傳入的sectionNameKeyPath:參數,是指明當前託管對象的哪一個屬性當作sectiontitle,在本文中就是Employee表的sectionName字段爲sectiontitle。從NSFetchedResultsSectionInfo協議的indexTitle屬性獲取這個值。

sectionNameKeyPath:設置屬性名後,就以這個屬性名做爲分組title,相同的title會被分到一個section中。

初始化FRC時參數managedObjectContext:傳入了一個MOC參數,FRC只能監測這個傳入的MOC發生的本地持久化改變。就像上面介紹時說的,其餘MOC對同一個持久化存儲區發生的改變,FRC則不能監測到這個變化。

再日後面看到cacheName:參數,這個參數我設置的是nil。參數的做用是開啓FRC的緩存,對獲取的數據進行緩存並指定一個名字。能夠經過調用deleteCacheWithName:方法手動刪除緩存。

可是這個緩存並無必要,緩存是根據NSFetchRequest對象來匹配的,若是當前獲取的數據和以前緩存的相匹配則直接拿來用,可是在獲取數據時每次獲取的數據均可能不一樣,緩存不能被命中則很難派上用場,並且緩存還佔用着內存資源

FRC初始化完成後,調用performFetch:方法來同步獲取持久化存儲區數據,調用此方法後FRC保存數據的屬性纔會有值。獲取到數據後,調用tableViewreloadData方法,會回調tableView的代理方法,能夠在tableView的代理方法中獲取到FRC的數據。調用performFetch:方法第一次獲取到數據並不會回調FRC代理方法。

代理方法

FRC中包含UITableView執行過程當中須要的相關數據,能夠經過FRCsections屬性,獲取一個遵照<NSFetchedResultsSectionInfo>協議的對象數組,數組中的對象就表明一個section

在這個協議中有以下定義,能夠看出這些屬性和UITableView的執行流程是緊密相關的。

@protocol NSFetchedResultsSectionInfo

/* Name of the section */
@property (nonatomic, readonly) NSString *name;

/* Title of the section (used when displaying the index) */
@property (nullable, nonatomic, readonly) NSString *indexTitle;

/* Number of objects in section */
@property (nonatomic, readonly) NSUInteger numberOfObjects;

/* Returns the array of objects in the section. */
@property (nullable, nonatomic, readonly) NSArray *objects;

@end // NSFetchedResultsSectionInfo

在使用過程當中應該將FRCUITableView相互嵌套,在FRC的回調方法中嵌套UITableView的視圖改變邏輯,在UITableView的回調中嵌套數據更新的邏輯。這樣能夠始終保證數據和UI的同步,在下面的示例代碼中將會演示FRCUITableView的相互嵌套。

Table View Delegate
// 經過FRC的sections數組屬性,獲取全部section的count值
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return fetchedResultController.sections.count;
}

// 經過當前section的下標從sections數組中取出對應的section對象,並從section對象中獲取全部對象count
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return fetchedResultController.sections[section].numberOfObjects;
}

// FRC根據indexPath獲取託管對象,並給cell賦值
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath];
    cell.textLabel.text = emp.name;
    return cell;
}

// 建立FRC對象時,經過sectionNameKeyPath:傳遞進去的section title的屬性名,在這裏獲取對應的屬性值
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return fetchedResultController.sections[section].indexTitle;
}

// 是否能夠編輯
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

// 這裏是簡單模擬UI刪除cell後,本地持久化區數據和UI同步的操做。在調用下面MOC保存上下文方法後,FRC會回調代理方法並更新UI
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
	if (editingStyle == UITableViewCellEditingStyleDelete) {
      // 刪除託管對象
	    Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
  	  [context deleteObject:emp];
  	  // 保存上下文環境,並作錯誤處理
  	  NSError *error = nil;
  	  if (![context save:&error]) {
  	      NSLog(@"tableView delete cell error : %@", error);
  	  }
 }
}

上面是UITableView的代理方法,代理方法中嵌套了FRC的數據獲取代碼,這樣在刷新視圖時就能夠保證使用最新的數據。而且在代碼中簡單實現了刪除cell後,經過MOC調用刪除操做,使本地持久化數據和UI保持一致。

就像上面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 文件

如今就建立好一個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表明數據源的實體。

Filter Predicate

更復雜的遷移需求

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

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

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

版本遷移總結

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


好多同窗都問我有Demo沒有,其實文章中貼出的代碼組合起來就是個Demo。後來想了想,仍是給本系列文章配了一個簡單的Demo,方便你們運行調試,後續會給全部博客的文章都加上Demo

Demo只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo一塊兒學習,只看Demo仍是不能理解更深層的原理Demo中幾乎每一行代碼都會有註釋,各位能夠打斷點跟着Demo執行流程走一遍,看看各個階段變量的值。

Demo地址劉小壯的Github


這兩天更新了一下文章,將CoreData系列的六篇文章整合在一塊兒,作了一個PDF版的《CoreData Book》,放在我Github上了。PDF上有文章目錄,方便閱讀。

若是你以爲不錯,請把PDF幫忙轉到其餘羣裏,或者你的朋友,讓更多的人瞭解CoreData,衷心感謝!

相關文章
相關標籤/搜索