4-6 Core Data--自定义 Core Data 迁移

自定义 Core Data 迁移似乎是一个不太起眼的话题。苹果在这方面只提供了很少的文档,若是初次涉足此方面内容,很可能会变成一个可怕的经历。鉴于客户端程序的性质,你无法测试你的用户所生成的数据集的所有可能排列。此外,解决迁移过程中出现的问题会很困难,而因为极有可能你的代码依赖于最新的数据模型,所以回退并不是一个可选的处理办法。

在本文中,我们将走一遍搭建自定义 Core Data 迁移的过程,并着重于数据模型的重构。我们将探讨从旧模型中提取数据并使用这些数据来填充具有新的实体和关系的目标模型。此外,会有一个包含单元测试的示例项目用于演示两个自定义迁移。

需要注意的是,如果对数据模型的修改只有增加一个实体或可选属性,轻量级的迁移是一个很好的选择。它们非常易于设置,所以本文只会稍稍提及它们。若想知道轻量级迁移的应用场合,请查看官方文档。

这就是说,如果你需要快速地在你的数据模型上进行相对复杂的改变,那么自定义迁移就是为你准备的。

映射模型 (Mapping Models)

当你要升级你的数据模型到新版,你将先选择一个基准模型。对于轻量级迁移,持久化存储会为你自动推断一个映射模型。然而,如果你对新模型所做的修改并不被轻量级迁移所支持,那么你就需要创建一个映射模型。一个映射模型需要一个源数据模型和一个目标数据模型。NSMigrationManager能够推断这两个模型间的映射模型。这使得它很诱人,可用来一路创建每一个以前的模型到最新模型之间的映射模型,但这很快就会变成一团乱麻。对于每一个新版模型,你需要创建的映射模型的量将线性增长。这可能看起来不是个大问题,但随之而来的是测试这些映射模型的复杂度大大提高了。

想像一下你刚刚部署一个包含版本 3 的数据模型的更新。你的某个用户已经有一段时间没有更新你的应用了,这个用户还在版本 1 的数据模型上。那么现在你就需要一个从版本 1 到版本 3 的映射模型。同时你也需要版本 2 到版本 3 的映射模型。当你添加了版本 4 的数据模型后,那你就需要创建三个新的映射模型。显然这样做的扩展性很差,那就来试试渐进式迁移吧。

渐进式迁移 (Progressive Migrations)

与其为每个之前的数据模型到最新的模型间都建立映射模型,还不如在每两个连续的数据模型之间创建映射模型。以前面的例子来说,版本 1 和版本 2 之间需要一个映射模型,版本 2 和版本 3 之间需要一个映射模型。这样就可以从版本 1 迁移到版本 2 再迁移到版本 3。显然,使用这种迁移的方式时,若用户在较老的版本上迁移过程就会比较慢,但它能节省开发时间并保证健壮性,因为你只需要确保从之前一个模型到新模型的迁移工作正常即可,而更前面的映射模型都已经经过了测试。

总的想法就是手动找出当前版本 v 和版本 v+1 之间的映射模型,在这两者间迁移,接着继续递归,直到持久化存储与当前的数据模型兼容。

这一过程看起来像下面这样(完整版可以在示例项目里找到):


4-6 Core Data--自定义 Core Data 迁移_第1张图片

这段代码主要来源于Marcus Zarra,他写了一本很棒的关于 Core Data 的书,查看这里。

自 iOS 7 和 OS Mavericks以来,Apple 将 SQLite 的日志模式改写为预写式日志 (Write-Ahead Logging), 这意味着数据库事务都被依附到一个 -wal 文件中。这有可能导致数据丢失和异常。为了数据的安全,我们会将日志模式改写为回溯模式。而如果我们想要迁移数据(或者为了以后备份),我们可以将一个字典传递给-addPersistentStoreWithType:configuration:URL:options:error:来完成改写。


迁移策略

NSEntityMigrationPolicy是自定义迁移过程的核心。苹果的文档中有这么一句话:

NSEntityMigrationPolicy的实例为一个实体映射自定义的迁移策略。

简单的说,这个类让我们不仅仅能修改实体的属性和关系,而且还能任意添加一些自定义的操作来完成每个实体的迁移。

迁移示例

假设我们有一个带有简单的数据模型的书籍应用。这个模型有两个实体:User和Book。Book实体有一个属性叫做authorName。我们想改善这个模型,添加一个新的实体:Author。同时我们想为Book和Author建立一个多对多的关系,因为一本书籍可有多个作者,而一个作者也可写多本书籍。我们将从Book对象里取出authorName用于填充一个新的实体并建立关系。

一开始我们要做的是基于第一个数据模型增加一个新版模型。在这个例子里,我们添加了一个Author实体,它与Book还有多对多的关系。

4-6 Core Data--自定义 Core Data 迁移_第2张图片

现在数据模型已经是我们所需要的,但我们还需要迁移所有已存在的数据,这就该NSEntityMigrationPolicy出场了。我们创建NSEntityMigrationPolicy的一个子类----MHWBookToBookPolicy。在映射模型里,我们选择Book实体并设置它作为公共部分(Utilities section)中的自定义策略。

4-6 Core Data--自定义 Core Data 迁移_第3张图片

同时我们使用 user info 字典来设置一个modelVersion,它将在未来的迁移中派上用场。

在MHWBookToBookPolicy中,我们将重载-createDestinationInstancesForSourceInstance:entityMapping:manager:error:方法,它允许我们自定义如何迁移每个 Book 实例。如果modelVersion的值不是 2,我们将调用父类的实现,否则我们就要做自定义迁移。我们插入基于映射的目标实体的新NSManagedObject对象到目标上下文。然后我们遍历目标实例的属性键值并与来自源实例的值一起填充它们。这将保证我们保留现存数据并避免设置任何我们已经在目标实例中移除的值。


4-6 Core Data--自定义 Core Data 迁移_第4张图片

然后我们将基于源实例的值创建一个Author实体。但若多本书有同一个作者会发生什么呢?我们将使用NSMigrationManager的一个 category 方法来创建一个查找字典,确保对于同一个名字的作者,我们只会创建一个Author。


4-6 Core Data--自定义 Core Data 迁移_第5张图片

最后,我们需要告诉迁移管理器在源存储与目的存储之间关联数据:


NSMigrationManager的 category 方法:


4-6 Core Data--自定义 Core Data 迁移_第6张图片

一个更复杂的迁移

过了一会,我们又想把fileURL这个属性从Book实体里提出来,放入一个叫做File的新实体里。同时我们还想修改实体之间的关系,以便User可与File有一对多的关系,而反过来File和Book有多对一的关系。

4-6 Core Data--自定义 Core Data 迁移_第7张图片

在之前的迁移中,我们只迁移了一个实体。而现在当我们添加了File后,事情变得有些复杂了。我们不能简单地在迁移一个Book时插入一个File实体并设置它与User的对应关系,因为此时User实体还没有被迁移,之间的关系也无从谈起。我们必须考虑迁移的执行顺序。在映射模型中,是可以改变实体映射的顺序的。具体到这里的例子,我们想将UserToUser映射放在BookToBook映射之上。这保证了User实体会比Book实体更早迁移。

4-6 Core Data--自定义 Core Data 迁移_第8张图片

添加一个File实体的途径和创建Author的过程相似。我们在MHWBookToBookPolicy中迁移Book实体时创建File对象。我们会查看源实例的User实体,为每个User实体创建一个新的File对象,并建立对应关系:


4-6 Core Data--自定义 Core Data 迁移_第9张图片

大数据集

如果你的存储包含了大量数据,以至到达一个临界点,迁移就会消耗过多内存,Core Data 提供了一个以数据块(chunks)的方式迁移的办法。苹果的文档有简要地提到这件事。解决办法是使用多映射模型分开你的迁移并为每个映射模型迁移一次。这要求你有一个对象图(object graph),在其中,迁移可被分为两个或多个部分。为了支持这一点而需要添加的代码其实很少。

首先,我们更新迁移方法以支持使用多个映射模型来迁移。已知映射模型的顺序很重要,我们将通过代理方法请求它们:


4-6 Core Data--自定义 Core Data 迁移_第10张图片

现在,我们如何知晓哪一个映射模型被用于这个特定的源模型呢?此处的 API 可能显得有些笨拙,但以下的解决方法确实完成了工作。在代理方法中,我们找出源模型的名字并返回相关的映射模型:


4-6 Core Data--自定义 Core Data 迁移_第11张图片

我们将为NSManagedObjectModel添加一个 category,以帮助我们找出它的文件名: We’ll add a category onNSManagedObjectModelthat helps us figure out its filename:


4-6 Core Data--自定义 Core Data 迁移_第12张图片

由于User在前面的例子(没有源关系映射)中被从对象图中隔离,因此迁移User的过程将省事很多。我们将从第一个映射模型中移除UserToUser映射,然后创建一个仅有UserToUser的映射。不要忘记在映射模型列表中返回新的User映射模型,因为我们正在其它映射中设置新关系

单元测试

为此应用建立单元测试异常简单:

将相关数据填入旧存储*。

将产生的持久性存储文件复制到你的测试目标

编写测试断言符合最新的数据模型。

运行测试,迁移数据到新的数据模型。

*这很容易完成,只需在模拟器里运行一下你应用最新的版本(production version)即可

步骤 1 和 2 很简单。步骤 3 留给读者作为练习,然后我会引导你通过第 4 步。

当持久化存储文件被添加到单元测试目标上时,我们需要告知迁移管理器把那个存储迁移至我们的目标存储。在示例项目中所示如下:


4-6 Core Data--自定义 Core Data 迁移_第13张图片

把下面的代码放到一个父类,以便于在测试的类中复用:


4-6 Core Data--自定义 Core Data 迁移_第14张图片

结论

轻量级迁移是直接在 SQLite 内部发生。这相对于自定义迁移来说非常快速且有效率。自定义迁移要把源对象读入到内存中,然后拷贝值到目标对象,重新建立关系,最后插入到新的存储中。这样做不仅很慢,而且当迁移大数据集时,由于内存大小的限制,它还会引起系统强制回收内存问题。

添加数据前尽量考虑完全

在处理任何数据持久性问题时最重要的事情之一就是仔细思考你的模型。我们希望模型是可持续发展的。在最开始创建模型的时候尽量考虑完全。添加空属性或者空实体也比以后进行迁移时候创建好的多,因为迁移很容易出现错误,而未使用的数据就不会了。

调试迁移

测试迁移时一个有用的启动参数是-com.apple.CoreData.MigrationDebug。设置为 1 时,你会在控制台收到关于迁移数据时特殊情况的信息。如果你熟悉 SQL 但不了解 Core Data,设置-com.apple.CoreData.SQLDebug为 1 可在控制台看到实际操作的 SQL 语句。


翻译作者:朱宏旭

你可能感兴趣的:(4-6 Core Data--自定义 Core Data 迁移)