2018第一篇技术文章,之前写过一篇关于CoreData基础的文章Magical Record 全面解析。关于CoreData迁移相关的文章网上有一些,但是都不是特别全面,所以这里总结一下,一方面自己巩固,一方面希望能帮到需要的同学。
CoreData迁移主要是两个方面,一个是数据库版本迁移,一个是数据迁移。
Migration is required when the model doesn't match the store.
首先推荐一个应用内调试的工具FLEX,可以直接查看数据库文件。
CoreData数据库版本迁移
数据库版本迁移比较简单。一般情况下是在新增了一张表之后,更新一下数据文件。
选中xcdatamodel文件之后,点击editor。可以看到如下选项。
。
这里包含了关于xcdatamodel大部分操作。选择Add Model Version。
完成之后可以看到
选中其中一个xcdatamodel文件,查看文件属性。这里包含了xcdatamodel各种属性,值得注意的是有个langua和coredatamodel,这两个后面会用到。
选择当前版本为新建的版本既可以了。完成之后小绿勾就会显示在更改的版本上。
CoreData数据迁移
凡是会引起NSManagedObjectModel托管对象模型变化的,都最好进行数据迁移,防止用户升级应用之后就闪退。会引起NSManagedObjectModel托管对象模型变化的有以下几个操作,新增了一张表,新增了一张表里面的一个实体,新增一个实体的一个属性,把一个实体的某个属性迁移到另外一个实体的某个属性里面等等
轻量级迁移
能够通过自动推断的迁移叫做轻量级迁移。如果只是做了很小的改变,比如给实体新增了属性,CoreData能够根据自动推断做自动数据迁移。轻量级迁移与普通迁移基本相同,不同之处在于我们自己不用提供映射模型( mapping model)。
如下场景可以使用轻量级迁移:
- 简单的新增、删除属性
- 重命名实体、属性
- 属性的可选与不可选之间的变化
- 可选变为不可选,并且定义了默认值。
特别注意,如果改变属性类型,CoreData不能自动推断,也就不是轻量级迁移。
轻量级迁移代码如下:
NSError *error = nil;
NSURL *storeURL = <#The URL of a persistent store#>;
NSPersistentStoreCoordinator *psc = <#The coordinator#>;
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
BOOL success = [psc addPersistentStoreWithType:<#Store type#>
configuration:<#Configuration or nil#> URL:storeURL
options:options error:&error];
if (!success) {
// Handle the error.
}
经过Demo测试可以总结出
如果不用自动迁移则会出现如下现象:
- 实体名字的修改会把之前的保存数据删除,原实体表删除。
- 属性名称的修改之后保存数据全部删除。
开启自动迁移则会:
- 实体名字的修改会把之前的保存数据删除,原实体表删除。
- 属性名称的修改之后数据能够成功迁移过来。
顺便提一下,magicRecord只提供了轻量级迁移的方式。
那么怎么判断CoreData是否可以进行自动迁移呢(其实所有的迁移都是都过NSMappingModel实现的,自动迁移也就是自动生成了NSMappingModel而已)。可以自定义,通过如下方法,分别传入源store,和目标store实现。
- (BOOL)migrateStore:(NSURL *)storeURL toVersionTwoStore:(NSURL *)dstStoreURL error:(NSError **)outError {
// Try to get an inferred mapping model.
NSMappingModel *mappingModel =
[NSMappingModel inferredMappingModelForSourceModel:[self sourceModel]
destinationModel:[self destinationModel] error:outError];
// If Core Data cannot create an inferred mapping model, return NO.
if (!mappingModel) {
return NO;
}
// Create a migration manager to perform the migration.
NSMigrationManager *manager = [[NSMigrationManager alloc]
initWithSourceModel:[self sourceModel] destinationModel:[self destinationModel]];
BOOL success = [manager migrateStoreFromURL:storeURL type:NSSQLiteStoreType
options:nil withMappingModel:mappingModel toDestinationURL:dstStoreURL
destinationType:NSSQLiteStoreType destinationOptions:nil error:outError];
return success;
}
如果使用了MagicRecord,可以使用 [MagicalRecord setupAutoMigratingCoreDataStack]一行代码实现轻量级迁移。
重量级迁移
如果CoreData不能自动推断,就需要用稍微复杂的防护四去迁移数据。原理就是需要定义怎么去转换数据,所有的信息包含在映射模型中,映射模型是一系列迁移信息的集合。上面提到的轻量级迁移是通过自动生成NSMappingModel实现的,重量级迁移需要我们自己去创建NSMappingModel。Xcode提供了可视化的工具来创建映射模型。
常见对象
类比对象模型,CoreData提供了针对,模型,实体,属性的迁移工具(NSMappingModel, NSEntityMapping, 和 NSPropertyMapping).。
- NSMappingModel:包含了NSEntityMapping,NSPropertyMapping的映射模型
- NSEntityMapping:包含源实体,目标实体还有映射的类型(新增,移除,拷贝,或者转换)
- NSPropertyMapping:包含在源实体和目标实体的名称,和一个表达式值。
除此之外还提供了自定义的方式。
可以在在Xcode的属性面板上直接使用自定义的表达式来做简单的迁移(复杂的迁移也是基于这些表达式)。
- 迁移从一个属性到另一个属性:比如amount属性重命名为totalCost。输入表达式$source.amount。
- 转换值:比如由temperature华氏摄氏度到摄氏度,表达式($source.temperature - 32.0) / 1.8.
一共有有6个预定义的key.
NSMigrationManagerKey: $manager
NSMigrationSourceObjectKey: $source
NSMigrationDestinationObjectKey: $destination
NSMigrationEntityMappingKey: $entityMapping
NSMigrationPropertyMappingKey: $propertyMapping
NSMigrationEntityPolicyKey: $entityPolicy
通过Xcode创建映射模型
这里以一个Student实体为例
先插入数据,便于查看变化:
[[NSManagedObjectContext MR_defaultContext] MR_saveWithBlock:^(NSManagedObjectContext * _Nonnull localContext) {
for (int i = 0; i < 100; i++) {
Student *sdt = [Student MR_createEntityInContext:localContext];
sdt.name = [NSString stringWithFormat:@"sdt_%d",i];
sdt.age = @(i);
}
} completion:^(BOOL contextDidSave, NSError * _Nullable error) {
if (contextDidSave) {
NSLog(@"Save Success");
}
}];
创建新的xcdatamodel文件
新建两个实体用于迁移Student中的age,name属性
新建映射模型文件,并且选择源和目的xcdatamodel:
创建完之后可以看到
这里的$source.age和上面介绍得输入表达式一样。可以使用上面预定义的6个key
需要特别注意一下下面的这个部分,迁移的规则都是从这里设置的
设置好source和destination
迁移的工作到这里就基本完成了,最后设置一下当前xcdatamodel文件的版。然后跑起来就可以在最新的数据库文件里面看到如下结果:
确实多了两张表
三张表的内容如下
数据确实从Student表里面迁移到了StudentAge表和StudentName表。
代码数据迁移
通过上面可视化的操作已经可以达到迁移的目的了,通过代码进行迁移主要是在数据迁移过程中,如果你还想做一些什么其他事情,比如说你想清理一下垃圾数据,实时展示数据迁移的进度。
- 直接上代码
检测是否需要迁移:
- (BOOL)isMigrationNecessaryForStore:(NSURL*)storeUrl
{
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path])
{
NSLog(@"SKIPPED MIGRATION: Source database missing.");
return NO;
}
NSError *error = nil;
NSDictionary *sourceMetadata =
[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:storeUrl error:&error];
NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel;
if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
{
NSLog(@"SKIPPED MIGRATION: Source is already compatible");
return NO;
}
return YES;
}
- 如何进行迁移:
- (BOOL)migrateStore:(NSURL*)sourceStore {
NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
BOOL success = NO;
NSError *error = nil;
// STEP 1 - 收集 Source源实体, Destination目标实体 和 Mapping Model文件
NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator
metadataForPersistentStoreOfType:NSSQLiteStoreType
URL:sourceStore
error:&error];
NSManagedObjectModel *sourceModel =
[NSManagedObjectModel mergedModelFromBundles:nil
forStoreMetadata:sourceMetadata];
NSManagedObjectModel *destinModel = _model;
NSMappingModel *mappingModel =
[NSMappingModel mappingModelFromBundles:nil
forSourceModel:sourceModel
destinationModel:destinModel];
// STEP 2 - 开始执行 migration合并, 前提是 mapping model 不是空,或者存在
if (mappingModel) {
NSError *error = nil;
NSMigrationManager *migrationManager =
[[NSMigrationManager alloc] initWithSourceModel:sourceModel
destinationModel:destinModel];
[migrationManager addObserver:self
forKeyPath:@"migrationProgress"
options:NSKeyValueObservingOptionNew
context:NULL];
NSURL *destinStore =
[[self applicationStoresDirectory]
URLByAppendingPathComponent:@"Temp.sqlite"];
success =
[migrationManager migrateStoreFromURL:sourceStore
type:NSSQLiteStoreType options:nil
withMappingModel:mappingModel
toDestinationURL:destinStore
destinationType:NSSQLiteStoreType
destinationOptions:nil
error:&error];
if (success)
{
// STEP 3 - 用新的migrated store替换老的store
if ([self replaceStore:sourceStore withStore:destinStore])
{
NSLog(@"SUCCESSFULLY MIGRATED %@ to the Current Model",
sourceStore.path);
[migrationManager removeObserver:self
forKeyPath:@"migrationProgress"];
}
}
else
{
NSLog(@"FAILED MIGRATION: %@",error);
}
}
else
{
NSLog(@"FAILED MIGRATION: Mapping Model is null");
}
return YES; // migration已经完成
}
- 迁移进度有变化,会通过观察者,observeValueForKeyPath来告诉用户进度,这里可以监听该进度,如果没有完成,可以来禁止用户执行某些操作。
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context {
if ([keyPath isEqualToString:@"migrationProgress"]) {
dispatch_async(dispatch_get_main_queue(), ^{
float progress =
[[change objectForKey:NSKeyValueChangeNewKey] floatValue];
int percentage = progress * 100;
NSString *string =
[NSString stringWithFormat:@"Migration Progress: %i%%",
percentage];
NSLog(@"%@",string);
});
}
}
代码迁移这块,读者可以自己试一试。
扩展阅读
Wha Is Core Data?
Next Core Data Model Versioning and Data Migration
iOS Core Data 数据迁移 指南