【优化篇】coreData数据迁移

coreData数据模型在迭代中修改了,如何保证新新版本的数据兼容旧版本的数据,可能你需要进行coreData的数据迁移。

前面有有写过一篇关于coredata简单使用的教程【进阶篇】iOS coreData简单使用教程

说起coredata的数据迁移,解决不当的后果是 闪退,这种问题出现的原因是在版本迭代的过程中,对数据模型进行了修改,而老版本的用户在进行App升级的时候,数据结构无法对应,导致App在一加载的时候就会直接闪退,无法进入App,这对用户体验来说是非常致命的。

Method one

下面看一个crash log


XXXXX[459:240351] CoreData: error: -addPersistentStoreWithType:SQLite configuration:(null) URL:file:///var/mobile/Containers/Data/Application/335A17E1-5F73-4CCB-BF2A-BA216CAD9810/Documents/Model.sqlite options:{
    NSInferMappingModelAutomaticallyOption = 1;
    NSMigratePersistentStoresAutomaticallyOption = 1;
} ... returned error Error Domain=NSCocoaErrorDomain Code=134130 "未能完成操作。(“Cocoa”错误 134130。)" UserInfo=0x18b26070 {URL=file:///var/mobile/Containers/Data/Application/335A17E1-5F73-4CCB-BF2A-BA216CAD9810/Documents/Model.sqlite, metadata={
    NSPersistenceFrameworkVersion = 519;
    NSStoreModelVersionHashes =     {
        HSDAccountInfo = <75d8e58f c7645625 e4b50374 2c971ccc 89681907 c866e0c1 51a76840 105af4b7>;
        HSDBaseEtt = <8300e6ad 52e70f8d b6f36112 4746b69f c927678b ee31688c 58266961 c88baf3f>;
        HSDInvestDetailInfo = ;
        HSDInvestInfo = <8b1f6048 2bfd9345 bf39aa1b 73867b56 3bcd5880 be28681f e3ffcb83 c4cc09ef>;
        HSDLoanDetailInfo = <67a48388 0aa3cee7 31113c81 9599a537 cee77239 7a12ba48 ff18c599 3ca1c6e5>;
        HSDLoanListInfo = ;
        HSDNewsInfo = ;
        HSDReturnAmtInfo = <2c0ede3a 48523624 42a1b64e 85f75755 a7594f01 8c10c62d 0fb71f99 5fb94535>;
        HSDTenderInfo = ;
        HSDTopImageInfo = <04839559 19fbcb38 2dc35bb1 b6202cf2 58d66a0d cf48455c 1b321f8a c34ace17>;
        HSDTopUpInfo = <0f56dc09 ac6d0793 543fe989 88814efa 2d159436 143d8cdf 840b86aa bb0e6373>;
        HSDWithdrawDepositInfo = <0ccb845a 94dc8ab4 accaaa21 a15b8b94 ac59ef5e 622825d2 3ee6830d 705f9ed0>;
    };
    NSStoreModelVersionHashesVersion = 3;
    NSStoreModelVersionIdentifiers =     (
        ""
    );
    NSStoreType = SQLite;
    NSStoreUUID = "BBD0C7D5-B836-4BB9-9C51-B0874103AF99";
    "_NSAutoVacuumLevel" = 2;
}, reason=Can't find model for source store} with userInfo dictionary {
    URL = "file:///var/mobile/Containers/Data/Application/335A17E1-5F73-4CCB-BF2A-BA216CAD9810/Documents/Model.sqlite";
    metadata =     {
        NSPersistenceFrameworkVersion = 519;
        NSStoreModelVersionHashes =         {
            HSDAccountInfo = <75d8e58f c7645625 e4b50374 2c971ccc 89681907 c866e0c1 51a76840 105af4b7>;
            HSDBaseEtt = <8300e6ad 52e70f8d b6f36112 4746b69f c927678b ee31688c 58266961 c88baf3f>;
            HSDInvestDetailInfo = ;
            HSDInvestInfo = <8b1f6048 2bfd9345 bf39aa1b 73867b56 3bcd5880 be28681f e3ffcb83 c4cc09ef>;
            HSDLoanDetailInfo = <67a48388 0aa3cee7 31113c81 9599a537 cee77239 7a12ba48 ff18c599 3ca1c6e5>;
            HSDLoanListInfo = ;
            HSDNewsInfo = ;
            HSDReturnAmtInfo = <2c0ede3a 48523624 42a1b64e 85f75755 a7594f01 8c10c62d 0fb71f99 5fb94535>;
            HSDTenderInfo = ;
            HSDTopImageInfo = <04839559 19fbcb38 2dc35bb1 b6202cf2 58d66a0d cf48455c 1b321f8a c34ace17>;
            HSDTopUpInfo = <0f56dc09 ac6d0793 543fe989 88814efa 2d159436 143d8cdf 840b86aa bb0e6373>;
            HSDWithdrawDepositInfo = <0ccb845a 94dc8ab4 accaaa21 a15b8b94 ac59ef5e 622825d2 3ee6830d 705f9ed0>;
        };
        NSStoreModelVersionHashesVersion = 3;
        NSStoreModelVersionIdentifiers =         (
            ""
        );
        NSStoreType = SQLite;
        NSStoreUUID = "BBD0C7D5-B836-4BB9-9C51-B0874103AF99";
        "_NSAutoVacuumLevel" = 2;
    };
    reason = "Can't find model for source store";
}

以上在在程序启动的时候打印的崩溃信息,可以看到说是没有找到对应的实体文件,在iOS9.0以下,如果对数据模型做了改动,仅仅在persistentStoreCoordinator中添加以下代码,是没有用的


 NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];

经多次验证,这中迁移方式,在iOS9.0以下仍然会存在闪退,所有需要更加高级的迁移方式,并且有些地方需要注意:

1、不要动原来的Model.xcdatamodeld文件,不能直接在这个文件中添加行的表,或者添加字段。
2、模型版本控制,保留原来的模型版本,在原来模型版本的基础上创建一个新的映射模型来做改动,选中Model.xcdatamodeld文件,Editor->Add Model Version,选择一个基础模型,命名为Model_to_Model2,意味着这是一个以Model为基础改动的模型。
image
3、创建好了之后,我们会得到Model_to_Model2.xcmappingmodel文件,点击它查看,其实它会和Model里面的内容一摸一样,接下来,我们将需要新增的表,或者字段,添加在这个映射模型中就行了,这样就完成了一次模型映射的数据迁移了。


Method tow

这个方法粗暴简单,不到特殊的情况(找不出解决办法的时候),推荐使用这个,数据迁移的报错都是因为数据模型文件不符的原因,我们在程序第一次启动的时候,将原来的旧的文件删除,在重新创建新的就行了。


#pragma mark -删除文件
- (void)deleteFileAtPath:(NSString *)filename
{
    NSFileManager *fileManager = [NSFileManager defaultManager];
    NSString *documentsDirectory = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask, YES) objectAtIndex:0];
    
    NSString *delelteFilePath = [documentsDirectory stringByAppendingPathComponent:filename];
    
    NSError *error;
    
    if ([fileManager removeItemAtPath:delelteFilePath error:&error] != YES)
        NSLog(@"Unable to delete file: %@", [error localizedDescription]);
    
}

.sqlite的文件在Document文件目录下,这个方法只需要传入文件的名字,就可以删除掉了。
注意:必须在程序启动时,在未调用coraData的上下文对象前调用这个方法来删除原来的文件,因为coredata的持久化对象使用的是懒加载方法,如果没有调用,就不会闪退,可以尽量将这个方法放在didFinishLaunchingWithOptions方法的靠前位置,删除了原来的数据库文件,原来版本所有的缓存数据将不复存在(怪我咯~)


Method three

渐进式迁移

关于这种方法,可以先来看一下当初我做数据迁移时候写的感想:

这次是数据迁移并没有成功,最后仍然是通过将原来的数据库文件删除的方式得到的解决。归纳其中的原因很可能是我动了原来的数据模型,然后对Coredata数据迁移的也并不是很了解,目前已经不知道在其中的某个改动过程出现了什么问题在使用coredata的过程中,非常需要注意的事情是,一旦提交了一个版本后,绝对不能再对原来的数据模型进行复制的标结构操作(如添加表,多表迁移改动数据)这样的操作必须要先添加一个Model version,然后再去在这个modek version中进行复制的表结构操作,如果是轻微的改动,使用轻量级的数据迁移即可,如果是复杂的改动,苹果官网的文档上推荐使用手动指引数据迁移,包括非常重要的多版本控制。
地址:苹果官网对数据迁移的文档描述
这个方法是一个渐进式迁移 (Progressive Migrations)
与其为每个之前的数据模型到最新的模型间都建立映射模型,还不如在每两个连续的数据模型之间创建映射模型。以前面的例子来说,版本 1 和版本 2 之间需要一个映射模型,版本 2 和版本 3 之间需要一个映射模型。这样就可以从版本 1 迁移到版本 2 再迁移到版本 3。显然,使用这种迁移的方式时,若用户在较老的版本上迁移过程就会比较慢,但它能节省开发时间并保证健壮性,因为你只需要确保从之前一个模型到新模型的迁移工作正常即可,而更前面的映射模型都已经经过了测试。
总的想法就是手动找出当前版本 v 和版本 v+1 之间的映射模型,在这两者间迁移,接着继续递归,直到持久化存储与当前的数据模型兼容。

迁移的代码我依然贴上,至今,我任然没有搞明白两个数据源的路径应该怎么破,如果有人明白,请教教再下。


/**
 *  ************数据迁移************
 *
 *  @param sourceStoreURL 源数据URL(.sqlite)
 *  @param type
 *  @param finalModel     目标数据模型(self.managedObjectModel)
 *  @param error
 *
 *  @return 是否成功
 */
- (BOOL)progressivelyMigrateURL:(NSURL *)sourceStoreURL
                         ofType:(NSString *)type
                        toModel:(NSManagedObjectModel *)finalModel
                          error:(NSError **)error
{
    
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:type
                                                                                              URL:sourceStoreURL
                                                                                            error:error];
    //NSLog(@"sourceMetadata%@",sourceMetadata);
    if (!sourceMetadata)
    {
        return NO;
    }
    //判断数据版本是否一致
    if ([finalModel isConfiguration:nil
        compatibleWithStoreMetadata:sourceMetadata]) {
        if (NULL != error) {
            *error = nil;
        }
        return YES;
    }
    
    NSManagedObjectModel *sourceModel = [self sourceModelForSourceMetadata:sourceMetadata];
    //NSLog(@"sourceModel%@",sourceModel);
    NSManagedObjectModel *destinationModel = nil;
    NSMappingModel *mappingModel = nil;
    NSString *modelName = nil;
    //获取映像模型
    if (![self getDestinationModel:&destinationModel
                      mappingModel:&mappingModel
                         modelName:&modelName
                    forSourceModel:sourceModel
                             error:error])
    {
        return NO;
    }
    //NSLog(@"sourceStoreURL:%@",sourceStoreURL);
    // 我们现在有了一个映射模型,开始迁移
    NSURL *destinationStoreURL = [self destinationStoreURLWithSourceStoreURL:sourceStoreURL
                                                                   modelName:modelName];
    //NSLog(@"destinationStoreURL:%@",destinationStoreURL);
    NSMigrationManager *manager = [[NSMigrationManager alloc] initWithSourceModel:sourceModel
                                                                 destinationModel:destinationModel];
//    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
//                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
//                             [NSNumber numberWithBool:NO], NSInferMappingModelAutomaticallyOption, nil];
    //NSLog(@"manager%@",manager);
    
    if (![manager migrateStoreFromURL:sourceStoreURL
                                 type:type
                              options:nil
                     withMappingModel:mappingModel
                     toDestinationURL:destinationStoreURL
                      destinationType:type
                   destinationOptions:nil
                                error:error]) {
        return NO;
    }
  
    // 现在迁移成功了,把文件备份一下以防不测
    
    if (![self backupSourceStoreAtURL:sourceStoreURL
          movingDestinationStoreAtURL:destinationStoreURL
                                error:error]) {
        return NO;
    }
    // 现在数据模型可能还不是“最新”版,所以接着递归
    return [self progressivelyMigrateURL:sourceStoreURL
                                  ofType:type
                                 toModel:finalModel
                                   error:error];
}

END