CoreDara版本迁移、数据迁移

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,163评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,301评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,089评论 0 352
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,093评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,110评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,079评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,005评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,840评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,278评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,497评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,394评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,980评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,628评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,649评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,548评论 2 352

推荐阅读更多精彩内容