iOS开发之——CoraData的数据迁移

1. 为什么要进行数据迁移?

在应用程序不断进行更新的过程中,其中的数据模型也可能在改变。假如是一些比较简单的改变,例如属性的默认值、验证规则、获取请求条件改变等等,这都可以直接实现。不够若是更为基础的结构上的改动,那就需要将旧的存储区迁移到新的模型中来才行。假如没有提供迁移数据所需要的映射和设置,就会造成新程序无法访问数据,或者说是两种数据存储区完全不兼容,而造成应用程序崩溃。

2. CoreData的迁移方式

2.1 轻量级迁移

自动完成存储区的迁移工作。设置方法:

NSDictionary *options = @{
NSMigratePersistentStoresAutomaticallyOption:@YES,
NSInferMappingModelAutomaticallyOption:@NO,
NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
};
NSError *error = nil;
_store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:options
error:&error];

主要就是以下两个设置选项:NSMigratePersistentStoresAutomaticallyOption:如果是@YESCore Data就会试着把低版本的(也就是与当前模型不兼容的)持久化存储区迁移到最新版本的模型;
NSInferMappingModelAutomaticallyOption如果是@YESCore Data就会试着以最为合理的方式自动推断出源模型实体(source model entity)中的某个属性到底对应于“目标模型实体”(destination model entity)中的哪一个属性。
把上述两个选项都打开并传递给NSPersistentStoreCoordinator,这种迁移方式就叫做轻量级迁移。假如在开发Core Data程序时使用了iCloud,那么只能采用这种迁移方式。

2.2 默认迁移方式

可以看到,轻量级迁移的方式实现起来比较简单方便,不过无法满足精细化迁移的要求。假如我们想将旧的实体中的某个属性连同其数据,整体迁移到新实体中的某个属性,那么轻量级的迁移方式就无法满足我们的需求了。旧实体和新实体之间的对应关系,叫做映射模型。这时候,默认迁移方式就派上用场了。
使用方法:

  • 首先,我们应将轻量级迁移中的NSInferMappingModelAutomaticallyOption选项设置为@NO,因为这个选项打开,它会检测有没有映射模型,关掉之后,可以确保使用我们接下来的映射模型进行迁移,而非系统的自动推断。
  • 其次新建一个映射模型文件(Mapping Model),然后保存,其中的SourceDataModel选择旧实体,即需要进行迁移的数据源实体,TargetDataModel选择新实体,即数据将要迁入的实体。
  • 修改实体。选中系统没有帮我们进行自动匹配的新实体,通过右侧的属性面板,手动选择Source,选择完毕之后,属性的名称会自动修改为OldEntityToNewEntity这种形式。
  • 修改实体属性。选中新实体中我们要进行数据迁入的属性,然后修改属性后边的Value Expression$source.旧实体属性

至此,迁移需要的映射模型我们已经创建好了。接下来,正常情况下,程序就会自动将我们设置的旧实体中的数据按照映射模型迁移至新实体中来。

2.4 迁移管理器方式

通过迁移管理器进行数据迁移,灵活性更大,它使开发者全权掌握迁移过程中创建的文件,从而令他们能够按自己的方式来灵活处理迁移中的各种问题。使用迁移管理器的一个好处,就是可以得知数据迁移的进度,进而可以告知用户。特别是当数据库比较大,变动比较复杂时,更有必要将迁移的进度通知给用户,以免他们在数据迁移时操作手机或者误以为手机卡死等。

2.4.1 判断是否需要进行迁移

伪代码:

if(系统没有这个存储区)
{
直接返回,不需要进行迁移;
}
剩余情况,则代表有存储区;
if(新模型与现有的存储区不兼容)
{
返回,不需要进行迁移
}
其他情况,需要进行迁移

真代码:

//如果不存在存储区,则就不需要进行迁移操作
if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path])
{
if (debug == 1)
{
NSLog(@"SKIPPED MIGRATION: Source database missing");
}
return NO;
}
NSError *error = nil;
NSDictionary *sourceMetadata =
[NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:storeUrl options:nil error:&error];
NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel;

//如果存储区原有的数据模型与现有的数据模型不兼容,同样不需要进行迁移
if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
{
if (debug == 1)
{
NSLog(@"SKIPPED MIGRATION: Source is already compatible");
return NO;
}
}
return YES;

2.4.2 进行迁移

伪代码:

  1. 收集执行数据迁移所需的信息,分别是:
    源模型:metadataForPersistentStoreOfType
    目标模型:_model;
    映射模型:mappingModelFromBundles,把源模型和目标模型一并传进去即可。
  2. 迁移。

    • 创建NSMigrationManger实例;
    • 设置一个临时的迁移存储区。
  3. 迁移完成,清理旧的存储区,并把新的临时存储区放回到原来的位置上,并且这新存储区的名字要和旧的一致,只有这样,Core Data才能使用这个新的存储区。当然,也可以将旧的存储区备份到其他地方,不过这将导致存储控件翻倍。

真代码:

//第一步,取得源数据,目的数据区和模型图
NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType URL:sourceStore options:nil error:&error];
NSManagedObjectModel *sourceModel = [NSManagedObjectModel mergedModelFromBundles:nil forStoreMetadata:sourceMetadata];
NSManagedObjectModel *destinModel = _model;

NSMappingModel *mappingModel = [NSMappingModel mappingModelFromBundles:nil forSourceModel:sourceModel destinationModel:destinModel];

//第二步,执行迁移操作,假设迁移映射模型mappingModel不为空
if (mappingModel)
{
NSError *error = nil;
NSMigrationManager *migrationManager = [[NSMigrationManager alloc]initWithSourceModel:sourceModel destinationModel:destinModel];
//针对迁移进度,通过KVO方式进行监听
[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)
{
//迁移完成后,清理存储区
if ([self replaceStore:sourceStore withStore:destinStore])
{
if (debug == 1)
{
NSLog(@"SUCCESSFULLY MIGRATED %@ to the Current Model", sourceStore.path);
}
[migrationManager removeObserver:self forKeyPath:@"migrationProgress"];
}
}
else
{
if (debug == 1)
{
NSLog(@"FAILED MIGRATION:%@", error);
}
}
}
else
{
if (debug == 1)
{
NSLog(@"FAILED MIGRATION: Mapping Model is null");
}
}
return YES;

其中,清理存储区的方法如下:

BOOL success = NO;
NSError *error = nil;
if ([[NSFileManager defaultManager] removeItemAtURL:old error:&error])
{
error = nil;
if ([[NSFileManager defaultManager] moveItemAtURL:new toURL:old error:&error])
{
success = YES;
}
else
{
if (debug == 1)
{
NSLog(@"FAILED to re-home new store %@", error);
}
}
}
else
{
if (debug == 1)
{
NSLog(@"FAILED to remove old store %@: error:%@", old, error);
}
}
return success;

其中迁移进度,我们可以在KVO监听的处理方法中进行显示:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context
{
if ([keyPath isEqualToString:@"migrationProgress"])
{
dispatch_async(dispatch_get_main_queue(), ^{
float progress = [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
//在这里,对进度进行显示
});
}
}

NSManagedObject扩展中的Delete规则

在我们使用Core Data创建相互联系的实体时,Delete Rule也是我们其中需要注意的一个点。它有四个选项可供我们选择:

  • Nullify:默认选项。如果删除了某个对象,而与该对象关联的其他对象,就会把指向该对象的“关系”清空,恢复成NULL
  • Cascade:这种规则会顺着关系,将对象删除。也就是它会将与删除对象关联的对象全部删除。
  • Deny:这种规则则会阻止开发者删除该对象。当我们经过查询,找到需要删除的对象之后,假如这个对象中关系规则是Deny,我们可以执行完删除操作之后,但是当我们将改动保存至数据库时,系统就会提示validation error(验证错误)。所以,如果想要删除这种对象,则应确保程序中已经没有与该对象关联的目标对象了。
  • No Action:这是一条有点奇怪的规则。与删除对象关联的对象什么也不改变,这时候就需要开发者手动设定反向的关系,确保它们都指向有效的对象。只有在极个别的情况下才需要使用这种规则。