认识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,衷心感谢!

相关文章
相关标签/搜索