CoreDara版本迁移、数据迁移

2018第一篇技术文章,之前写过一篇关于CoreData基础的文章Magical Record 全面解析。关于CoreData迁移相关的文章网上有一些,但是都不是特别全面,所以这里总结一下,一方面自己巩固,一方面希望能帮到需要的同学。

CoreData迁移主要是两个方面,一个是数据库版本迁移,一个是数据迁移。

Migration is required when the model doesn't match the store.

首先推荐一个应用内调试的工具FLEX,可以直接查看数据库文件。

CoreDara版本迁移、数据迁移_第1张图片

CoreData数据库版本迁移

数据库版本迁移比较简单。一般情况下是在新增了一张表之后,更新一下数据文件。

选中xcdatamodel文件之后,点击editor。可以看到如下选项。


CoreDara版本迁移、数据迁移_第2张图片

这里包含了关于xcdatamodel大部分操作。选择Add Model Version。

完成之后可以看到


选中其中一个xcdatamodel文件,查看文件属性。这里包含了xcdatamodel各种属性,值得注意的是有个langua和coredatamodel,这两个后面会用到。


CoreDara版本迁移、数据迁移_第3张图片

选择当前版本为新建的版本既可以了。完成之后小绿勾就会显示在更改的版本上。

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的属性面板上直接使用自定义的表达式来做简单的迁移(复杂的迁移也是基于这些表达式)

CoreDara版本迁移、数据迁移_第4张图片

  • 迁移从一个属性到另一个属性:比如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");
        }
    }];
CoreDara版本迁移、数据迁移_第5张图片

创建新的xcdatamodel文件


CoreDara版本迁移、数据迁移_第6张图片

新建两个实体用于迁移Student中的age,name属性


新建映射模型文件,并且选择源和目的xcdatamodel:


CoreDara版本迁移、数据迁移_第7张图片
CoreDara版本迁移、数据迁移_第8张图片
CoreDara版本迁移、数据迁移_第9张图片

创建完之后可以看到


CoreDara版本迁移、数据迁移_第10张图片

这里的$source.age和上面介绍得输入表达式一样。可以使用上面预定义的6个key

需要特别注意一下下面的这个部分,迁移的规则都是从这里设置的

CoreDara版本迁移、数据迁移_第11张图片

设置好source和destination


迁移的工作到这里就基本完成了,最后设置一下当前xcdatamodel文件的版。然后跑起来就可以在最新的数据库文件里面看到如下结果:

确实多了两张表


CoreDara版本迁移、数据迁移_第12张图片

三张表的内容如下


CoreDara版本迁移、数据迁移_第13张图片

CoreDara版本迁移、数据迁移_第14张图片

CoreDara版本迁移、数据迁移_第15张图片

数据确实从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 数据迁移 指南

你可能感兴趣的:(CoreDara版本迁移、数据迁移)