跳转至

6.版本控制和迁移

您已经了解了如何在Core Data应用程序中设计数据模型和NSManagedObject子类。 在应用程序开发期间,在发布日期之前,彻底的测试可以帮助解决数据模型。 然而,应用发布后应用使用、设计或功能的变化将不可避免地导致数据模型的变化。 那你怎么办?

您无法预测未来,但借助Core Data,您可以在每次发布新应用时,向未来迁移。迁移过程将更新用以前版本的数据模型创建的数据,以匹配当前数据模型。

本章通过介绍笔记应用程序数据模型的演变过程,讨论核心数据迁移的多个方面。

您将从一个数据模型中只有一个实体的简单应用程序开始。 随着您向应用添加更多功能和数据,本章中执行的迁移将逐渐变得更加复杂。

大迁徙开始!

何时迁移

什么时候需要迁移? 对这个常见问题最简单的回答是“当您需要对数据模型进行更改时。“

但是,在某些情况下,您可以避免迁移。 如果应用仅将Core Data用作离线缓存,则在更新应用时,只需删除并重建数据存储即可。 只有当用户数据的真实来源不在数据存储中时,这才可能。 在所有其他情况下,您需要保护用户的数据。

也就是说,任何时候不改变数据模型就不可能实现设计更改或功能请求,您都需要创建数据模型的新版本并提供迁移路径。

迁移流程

初始化Core Data堆栈时,涉及的步骤之一是将存储添加到持久存储协调器。 当您遇到此步骤时,Core Data会在将存储添加到协调器之前执行一些操作。 首先,Core Data分析商店的模型版本。 接下来,它将这个版本与协调器配置的数据模型进行比较。 如果商店的模型版本和协调器的模型版本不匹配,Core Data将在启用时执行迁移。

Note

如果未启用迁移,并且存储与模型不兼容,则Core Data将简单地不将存储附加到协调器,并使用适当的原因代码指定错误。

要开始迁移过程,Core Data需要原始数据模型和目标模型。 它使用这两个版本来加载或创建迁移的映射模型,使用该模型将原始存储中的数据转换为可以存储在新存储中的数据。 一旦Core Data确定了映射模型,迁移过程就可以开始了。

迁移分为三个步骤:

  1. 首先,Core Data将所有对象从一个数据存储复制到下一个。
  2. 接下来,Core Data根据关系映射连接并关联所有对象。
  3. 最后,在目标模型中强制执行所有数据验证。 核心数据在数据复制期间禁用目标模型验证。

您可能会问:“如果出现问题,原始源数据存储会发生什么情况?“ 对于几乎所有类型的核心数据迁移,除非迁移顺利完成,否则原始存储不会发生任何变化。 只有迁移成功时,Core Data才会删除原始数据存储。

迁移类型

根据我自己的经验,我发现除了Apple所说的轻量级和重量级迁移之间的简单区别之外,还有一些迁移变体。 下面,我提供了迁移名称的更微妙的变体,但这些名称无论如何都不是正式的类别。 您将从最不复杂的迁移形式开始,以最复杂的形式结束。

轻量级迁移

轻量级迁移是Apple的术语,用于迁移时您所涉及的工作量最少。 当你使用NSPersistentContainer时,这会自动发生,或者你必须在构建自己的Core Data堆栈时设置一些标志。 对于数据模型的更改量有一些限制,但由于启用此选项所需的工作量很小,因此它是理想的设置。

手动迁移

手动迁移需要您做更多的工作。 您需要指定如何将旧的数据集映射到新的数据集,但是您可以获得一个更显式的映射模型文件来配置。 在Xcode中设置映射模型很像设置数据模型,使用类似的GUI工具和一些自动化。

自定义手动迁移

这是迁移复杂性指数中的第3级。 您仍将使用映射模型,但使用自定义代码来补充它,以指定数据的自定义转换逻辑。 自定义实体转换逻辑涉及创建NSEntityMigrationPolicy子类并在其中执行自定义转换。

全手动迁移

完全手动迁移适用于那些即使指定自定义转换逻辑也不足以将数据从一个模型版本完全迁移到另一个模型版本的情况。 自定义版本检测逻辑和迁移过程的自定义处理是必要的。 在本章中,您将设置一个完全手动的迁移,以跨非连续版本更新数据,例如从版本1跳到版本4。

在本章中,您将了解每种迁移类型以及何时使用它们。 我们开始吧!

入门

本书的参考资料中包括一个名为UnCloudNotes的启动项目。 找到starter项目并在Xcode中打开它。

iPhone模拟器中构建并运行应用程序。 您将看到一个空的注释列表:

img

点击右上角的加号(+)按钮以添加新便笺。 添加标题(注释正文中有默认文本,可加快处理速度),然后点击Create将新注释保存到数据存储中。 重复此操作几次,这样您就有了一些要迁移的示例数据。

回到Xcode,打开 UnCloudNotesDatamodel.xcdatamodeld 文件,显示Xcode中的实体建模工具。 数据模型很简单--只有一个实体,一个Note,有几个属性。 img

您将向应用程序添加一个新功能:将照片附加到便笺的功能。 数据模型没有任何地方可以保存这类信息,因此您需要在数据模型中添加一个位置来保存照片。 但你已经在应用程序中添加了一些测试笔记。如何在不破坏现有注释的情况下更改模型?

是时候进行第一次迁移了!

轻量级迁移

Xcode中,选择UnCloudNotes数据模型文件(如果还没有)。 这将显示主工作区中的Entity Modeler。 接下来,打开编辑器菜单,选择Add Model Version…. 将新版本命名为UnCloudNotesDataModel v2,并确保在Based on model字段中选择UnCloudNotesDataModel。 Xcode现在将创建数据模型的副本。

Note

你可以给予这个文件任何你想要的名字。 顺序的v2v3v4等命名有助于您轻松区分版本。

这一步将创建数据模型的第二个版本,但您仍然需要告诉Xcode使用新版本作为当前模型。 如果忘记了这一步,选择顶层 UnCloudNotesDataModel.xcdatamodeld 文件将执行您对原始模型文件所做的任何更改。 您可以通过选择单个模型版本来覆盖此行为,但最好确保不会意外修改原始文件。

为了执行任何移植,您需要保持原始模型文件不变,并对全新的模型文件进行更改。

在右侧的File Inspector窗格中,底部有一个名为“模型版本”的选择菜单。

更改该选择以匹配新数据模型的名称UnCloudNotesDataModel v2

img

一旦你做了这个改变,注意项目导航器中的绿色小复选标记图标已经从以前的数据模型移动到了v2数据模型: img

在设置堆栈时,Core Data将尝试首先将持久存储与勾选的模型版本连接。 如果找到了存储文件,并且它与此模型文件不兼容,则将触发迁移。 旧版本用于支持迁移。 当前模型是Core Data将确保在附加堆栈的其余部分供您使用之前加载的模型。

确保您选择了v2数据模型,并在Note实体中添加 image 属性。 将属性名称设置为image,属性类型设置为 Transformable

由于该属性将包含图像的实际二进制位,因此您将使用自定义的NSValueTransformer将二进制位转换为UIImage并再次转换。 在ImageTransformer中已经为您提供了这样一个转换器。 在屏幕右侧的数据模型检查器中,查找Value Transformer字段,然后输入 ImageTransformer。 接下来,在模块字段中选择Current Product Module

img

Note

当从模型文件中引用代码时,就像在XibStoryboard文件中一样,您需要指定一个 module UnCloudNotesCurrent Product Module,取决于您的下拉列表提供的内容),以允许类加载器找到您想要附加的确切代码。

新模型现在可以开始编写代码了! 打开 Note.swift 并在displayIndex下面添加以下属性:

@NSManaged var image: UIImage?

构建并运行应用程序。你会看到你的笔记仍然神奇地显示! 原来轻量级迁移是默认启用的。 这意味着每次你创建一个新的数据模型版本,并且它可以自动迁移,它就会自动迁移。 多节省时间啊!

推断映射模型

NSPersistentStoreDescription上启用shouldInferMappingModelAutomatically标志时,很多情况下Core Data都可以推断出映射模型。 Core Data可以自动查看两个数据模型的差异,并在它们之间创建映射模型。

对于模型版本之间相同的实体和属性,这是一个直接的数据传递映射。 对于其他更改,只需遵循Core Data的一些简单规则来创建映射模型。

在新模型中,更改必须符合明显的迁移模式,例如:

  • 删除实体、属性或关系
  • 使用renamingIdentifier重命名实体、属性或关系
  • 添加新的可选属性
  • 添加具有默认值的新的必需属性
  • 将可选属性更改为非可选属性并指定默认值
  • 将非可选属性更改为可选属性
  • 更改实体层次结构
  • 添加新的父实体并在层次结构中上下移动属性
  • 将关系从一对一更改为多对多
  • 将关系从非有序对多更改为有序对多(反之亦然)

Note

查看Apple的文档,了解有关Core Data如何推断轻量级迁移映射的更多信息网站上:https://developer.apple.com/documentation/coredata/using_lightweight_migration

正如您从这个列表中看到的,Core Data可以检测到数据模型之间的各种常见更改,更重要的是,可以自动做出反应。

根据经验,如果需要,所有迁移都应该从轻量级迁移开始,只有在需要时才移动到更复杂的映射。

至于从UnCloudNotes迁移到UnCloudNotes v2image属性的默认值为nil,因为它是一个可选属性。 这意味着Core Data可以轻松地将旧数据存储迁移到新数据存储,因为这种更改遵循轻量级迁移模式列表中的第3项。

图片附件

现在数据已迁移,您需要更新UI以允许将图像附件添加到新的笔记。 幸运的是,大部分的工作已经为你完成了。

打开 Main.storyboard 并找到Create Note场景。 在下面,您将看到Create Note With Images场景,其中包括附加图像的界面。

Create Note场景附加到具有根视图控制器关系的导航控制器。 从导航控制器Control-dragCreate Note With Images场景并选择根视图控制器关系segue

这将断开旧的Create Note场景,并连接新的图像驱动的场景。

img

然后打开 AttachPhotoViewController.swift 并在UIImagePickerControllerDelegate扩展中添加如下方法:

func imagePickerController(
  _ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info:
  [UIImagePickerController.InfoKey: Any]
) {
  guard let note = note else { return }

  note.image =
    info[UIImagePickerController.InfoKey.originalImage]
    as? UIImage

  _ = navigationController?.popViewController(
    animated: true)
}

一旦用户从标准图像选取器中选择了一个图像,这将填充注释的新图像属性。

然后打开 CreateNoteViewController.swift 并将viewDidAppear(_:)替换为以下内容:

override func viewDidAppear(_ animated: Bool) {
  super.viewDidAppear(animated)

  guard let image = note?.image else {
    titleField.becomeFirstResponder()
    return
  }

  attachedPhoto.image = image
  view.endEditing(true)
}

如果用户已将新图像添加到注释,则将显示新图像。

接下来打开 NotesListViewController.swift 并更新tableView(_:cellForRowAt):如下:

override func tableView(
  _ tableView: UITableView,
  cellForRowAt indexPath: IndexPath
) -> UITableViewCell {
  let note = notes.object(at: indexPath)
  let cell: NoteTableViewCell
  if note.image == nil {
    cell = tableView.dequeueReusableCell(
      withIdentifier: "NoteCell",
      for: indexPath
    ) as! NoteTableViewCell
  } else {
    cell = tableView.dequeueReusableCell(
      withIdentifier: "NoteCellWithImage",
      for: indexPath
    ) as! NoteImageTableViewCell
  }

  cell.note = note
  return cell
}

这将根据笔记是否存在图像,使正确的UITableViewCell子类出列。 最后打开 NoteImageTableViewCell.swift,并在updateNoteInfo(note:)末尾添加以下内容:

noteImage.image = note.image

这将使用便笺中的图像更新NoteImageTableViewCell中的UIImageView。 生成并运行,然后选择添加新注释:

img

点击Attach Image按钮,将图像添加到便笺中。 从模拟照片库中选择一张图像,您将在新便笺中看到它:

img

该应用程序使用标准的UIImagePickerController将照片作为附件添加到笔记。

Note

要将您自己的图像添加到模拟器的相册中,请将图像文件拖到打开的模拟器窗口中。 值得庆幸的是,iOS模拟器附带了一个照片库供您使用。

如果您使用的是设备,请打开 AttachPhotoViewController.swift,并将图片拾取器控制器上的sourceType属性设置为。照相机以使用设备照相机拍摄照片。 现有的代码使用相册,因为模拟器中没有摄像头。

添加几个带有照片的示例注释,因为在下一节中,您将使用示例数据来进行稍微复杂一点的迁移。

Note

此时,您可能希望将v2源代码的副本复制到不同的文件夹中,以便稍后返回。 或者,如果您正在使用源代码管理,请在此处设置一个标记,以便您可以返回到此点。 您可能还希望保存数据存储文件的副本,并为该版本的应用程序追加名称v2,因为稍后将在更复杂的迁移中使用该名称。

祝贺;您已成功迁移数据并基于迁移的数据添加了新功能。

手动迁移

此数据模型的下一步发展是从将单个图像附加到注释转移到附加多个图像。 注释实体将保留,而您需要为图像创建一个新实体。 由于一个笔记可以有许多图像,因此将存在对多关系。

将一个实体拆分为两个实体并不完全在轻量级迁移所能支持的列表中。 是时候升级到自定义手动迁移了!

每次迁移的第一步都是创建一个新的模型版本。 如前所述,选择 UnCloudNotesDataModel.xcdatamodeld 文件,并从Editor菜单项中选择Add Model Version。... 将此模型命名为UnCloudNotesDataModel v3,并基于v2数据模型。 使用文件检查器窗格中的选项将新模型版本设置为默认模型。

接下来,您将向新数据模型添加一个新实体。 在左下角,单击“添加实体”按钮。 将此实体重命名为Attachment。 选择实体,并在“数据模型检查器”窗格中,将“类名”设置为“附件”,将“模块”设置为Current Product Module

img

Attachment实体中创建两个属性。 添加一个非可选属性image,类型为Transformable,自定义类转换器字段设置为ImageTransformer,模块字段设置为Current Product Module。 这与您之前添加到Note实体的image属性相同。 添加第二个非可选属性dateCreated,并使其成为Date类型。

接下来,从Attachment实体向Note实体添加关系。 将关系名称设置为note,将其目标设置为Note

选择Note实体,删除image属性。 最后,创建从Note实体到Attachment实体的一对多关系。 将其标记为Optional。 将关系命名为attachments,将目标设置为Attachment,并选择您刚刚创建的note关系作为反向关系。

img

数据模型现在可以迁移了! 当Core Data模型准备就绪时,您的应用中的代码将需要一些更新,以使用对数据实体的更改。 请记住,您不再使用Note上的image属性,而是使用多个attachments

新建一个名为 Attachment.swift 的文件并将其内容替换为:

import Foundation
import UIKit
import CoreData

class Attachment: NSManagedObject {
  @NSManaged var dateCreated: Date
  @NSManaged var image: UIImage?
  @NSManaged var note: Note?
}

接下来,打开 Note.swift 并将image属性替换为以下内容:

@NSManaged var attachments: Set<Attachment>?

应用的其余部分仍然依赖于image属性,因此如果尝试构建应用,您将获得编译错误。在attachments下面的Note类中添加以下内容:

var image: UIImage? {
  return latestAttachment?.image
}

var latestAttachment: Attachment? {
  guard let attachments = attachments,
    let startingAttachment = attachments.first else {
      return nil
  }

  return Array(attachments).reduce(startingAttachment) {
    $0.dateCreated.compare($1.dateCreated)
      == .orderedAscending ? $0 : $1
  }
}

此实现使用了一个computed属性,该属性从最新的附件中获取图像。

如果有多个附件,latestAttachment会抓取最新的一个并返回。

然后打开AttachPhotoViewControllerswift。 当用户选择一个图像时,更新它以创建一个新的Attachment对象。 将Core Data导入添加到文件的顶部:

import CoreData

接下来,将imagePickerController(_:didFinishPickingMediaWithInfo:)替换为:

func imagePickerController(
  _ picker: UIImagePickerController,
  didFinishPickingMediaWithInfo info:
  [UIImagePickerController.InfoKey: Any]
) {
  guard let note = note,
    let context = note.managedObjectContext else {
      return
  }

  let attachment = Attachment(context: context)
  attachment.dateCreated = Date()
  attachment.image =
    info[UIImagePickerController.InfoKey.originalImage] 
    as? UIImage
  attachment.note = note

  _ = navigationController?.popViewController(animated: true)
}

该实现创建一个新的Attachment实体,将UIImagePickerController中的图像添加为image属性,然后将Attachmentnote属性设置为当前note

映射模型

通过轻量级迁移,Core Data可以自动创建映射模型,以便在更改简单时将数据从一个模型版本迁移到另一个模型版本。 当更改不那么简单时,您可以使用映射模型手动设置从一个模型版本迁移到另一个模型版本的步骤。

在创建映射模型之前,必须完成并最终确定目标模型,这一点很重要。

在创建一个新的MappingModel的过程中,您实际上将把源和目标模型版本锁定到MappingModel文件中。

这意味着在创建映射模型之后对实际数据模型所做的任何更改都不会被映射模型看到。

现在您已经完成了对v3数据模型的更改,您知道轻量级迁移并不能完成这项工作。 要创建映射模型,请在Xcode中打开文件菜单,然后选择 New ▸ File

导航到iOS\Core Data部分并选择Mapping Model

img

点击下一步,选择v2数据模型作为源模型,选择v3数据模型作为目标模型。

将新文件命名为 UnCloudNotesMappingModel_v2_to_v3。 我通常使用的文件命名约定是数据模型名称沿着源版本和目标版本。 随着应用程序收集越来越多的映射模型,这种文件命名约定可以更容易地区分文件和它们随时间变化的顺序。

打开 UnCloudNotesMappingModel_v2_to_v3.xcmappingmodel. 幸运的是,映射模型并不是完全从头开始; Xcode检查源模型和目标模型,并尽可能地进行推断,因此您将从一个由基础组成的映射模型开始。

属性映射

有两个映射,一个名为NoteToNote,另一个名为AttachmentNoteToNote介绍如何将v2 Note实体迁移到v3 Note实体。

选择NoteToNote,您将看到两个部分: Attribute Mappings Relationship Mappings

img

这里的属性映射相当简单。 注意带有模式$source的值表达式。 $source是映射模型编辑器的特殊标记,表示对源实例的引用。 请记住,使用Core Data,您不是在处理数据库中的行和列。 相反,您处理的是对象、它们的属性和类。

在这种情况下,bodydateCreateddisplayIndextitle的值将直接从源代码传输。 这些都是简单的案例!

attachments关系是新的,所以Xcode不能从源代码中填充任何东西。 但是,您将不会使用这个特定的关系映射,因此删除这个映射。 您将很快得到正确的关系映射。

选择Attachment映射,并确保右侧的“实用程序”面板处于打开状态。

选择“实用程序”面板中的最后一个选项卡,打开Entity Mapping检查器:

img

在下拉列表中选择Note作为源实体。 一旦你选择了源实体,Xcode将尝试根据源实体和目标实体的属性名称自动解析映射。 在这种情况下,Xcode会为你填写dateCreatedimage映射:

img

Xcode也会将实体映射从Attachment重命名为NoteToAttachmentXcode再次提供帮助;它只需要从你的小推动来指定源实体。 由于属性名称匹配,Xcode将为您填充值表达式。 将数据从Note实体映射到Attachment实体是什么意思? 可以把它想象成这样:“对于每个Note,创建一个Attachment并复制imagedateCreated属性。“

此映射将为每个Note创建一个Attachment,但如果有图像附加到笔记,则实际上只需要一个Attachment。 确保选择了NoteToAttachment实体映射,并在检查器中将Filter Predicate字段设置为 image!= nil 。 这将确保“附件”映射仅在源中存在图像时发生。

关系映射

迁移能够将图像从Notes复制到Attachments,但到目前为止,NoteAttachment之间没有任何关系。 获得该行为的下一步是添加关系映射。

NoteToAttachment映射中,您将看到一个名为note的关系映射。 就像你在NoteToNote中看到的关系映射一样,值表达式是空的,因为Xcode不知道如何自动迁移关系。

选择 NoteToAttachment映射。 在关系列表中选择note关系行,以便检查器更改以反映关系映射的属性。 在取源字段中,选择Auto Generate Value Expression。 在Key Path字段输入$source,在Mapping Name字段选择 NoteToNote

img

这将生成一个如下所示的值表达式:

FUNCTION($manager,
  "destinationInstancesForEntityMappingNamed:sourceInstances:",
  "NoteToNote", $source)

FUNCTION 语句类似于objc_msgSend语法;也就是说,第一个参数是对象实例,第二个参数是选择器,任何进一步的参数都作为参数传递给该方法。

因此,映射模型正在调用$manager对象上的方法。 $manager标记是对处理迁移过程的NSMigrationManager对象的特殊引用。

Note

使用FUNCTION表达式仍然依赖于Objective-C语法的一些知识。 苹果可能还需要一段时间才能将核心数据100%移植到Swift!

Core Data在迁移过程中创建迁移管理器。 迁移管理器跟踪哪些源对象与哪些目的地对象相关联。 方法destinationInstancesForEntityMappingNamed:sourceInstances:将查找源对象的目标实例。

上一页中的表达式表示“将note关系设置为NoteToNote映射迁移到的该映射的$source对象的任何对象”,在本例中,它将是新数据存储中的Note实体。 您已完成自定义映射! 现在您已经有了一个映射,该映射被配置为将一个实体拆分为两个实体,并将适当的数据对象关联在一起。

最后一件事

在运行此迁移之前,您需要更新Core Data设置代码以使用此映射模型,而不是尝试自行推断。

打开 CoreDataStack。swift 并查找您首先设置启用迁移标志的storeDescription属性。 将标志更改为以下内容:

description.shouldMigrateStoreAutomatically = true
description.shouldInferMappingModelAutomatically = false

通过将shouldInfermappingModelAutomatically设置为false,您已经确保了持久存储协调器现在将使用新的映射模型来迁移存储。 是的,这就是您需要更改的所有代码;没有新代码!

当核心数据被告知不要推断或生成映射模型时,它将在默认或主包中查找映射模型文件。 映射模型包含模型的源版本和目标版本。 核心数据将使用该信息来确定使用哪个映射模型(如果有的话)来执行迁移。 这真的很简单,只需更改一个选项即可使用自定义映射模型。

严格地说,这里不需要将shouldMigrateStoreAutomatically设置为true,因为true是默认值。 不过,这么说吧我们以后还需要这个。

构建并运行应用程序。你会注意到表面上并没有太大的变化! 但是,如果您仍然像以前一样看到笔记和图像,则映射模型起作用了。 Core Data已经更新了SQLite存储的底层模式,以反映v3数据模型中的更改。

Note

同样,您可能希望将v3源代码的副本复制到不同的文件夹中,以便稍后返回。 或者,如果您正在使用源代码管理,请在此处设置一个标记,以便您可以返回到此点。 同样,您可能还希望保存数据存储文件的副本,并为该版本的应用程序追加名称“v3”,因为稍后您将在更复杂的迁移中使用该名称。

复杂的映射模型

上面已经为UnCloudNotes想出了一个新功能,所以你知道这意味着什么。 是时候再次迁移数据模型了! 这一次,他们决定只支持图片附件是不够的。 他们希望未来版本的应用程序支持视频,音频文件或真正添加任何有意义的附件。

您决定拥有一个名为Attachment的基本实体和一个名为ImageAttachment的子类。 这将使每种附件类型都有自己的有用信息。 图像可以具有用于标题、图像大小、压缩级别、文件大小等的属性。 以后,您可以为其他附件类型添加更多子类。

虽然新映像将在保存之前获取此信息,但您需要在迁移期间从当前映像中提取此信息。 您需要使用CoreImageImageIO库。 这些是Core Data绝对不支持的数据转换,这使得自定义手动迁移成为适合这项工作的工具。

通常,任何数据迁移的第一步都是在Xcode中选择数据模型文件,然后选择 Editor ▸ Add Model Version。... 这一次,创建名为UnCloudNotesDataModel v4的数据模型版本4。 不要忘记在Xcode Inspector中将数据模型的当前版本设置为v4

打开v4数据模型,添加一个新的实体ImageAttachment。 设置类为ImageAttachment,设置模块为Current Product Module。 对ImageAttachment进行以下更改:

  1. 将父实体设置为Attachment
  2. 添加必需的String属性caption
  3. 添加必需的Float属性width
  4. 添加必需的Float属性名称height
  5. 添加可选的可转换属性image
  6. ValueTransformer设置为ImageTransformer,将Module设置为Current Product Module

接下来,在Attachment实体中:

  1. 删除image属性。
  2. 如果自动创建了newRelationship,请将其删除。

img

父实体类似于拥有父类,这意味着ImageAttachment将继承Attachment的属性。 当您稍后设置托管对象子类时,您将看到此继承在代码中显式显示。

在为映射模型创建自定义代码之前,如果现在就创建ImageAttachment源文件,会更容易。 新建一个名为 ImageAttachmentSwift文件,并将其内容替换为以下内容:

import UIKit
import CoreData

class ImageAttachment: Attachment {
  @NSManaged var image: UIImage?
  @NSManaged var width: Float
  @NSManaged var height: Float
  @NSManaged var caption: String
}

然后打开 Attachment.swift 并删除image属性。 由于它已被移动到ImageAttachment,并从v4数据模型中的Attachment实体中删除,因此应将其从代码中删除。 对于新的数据模型,这应该可以做到。 完成后,您的版本4数据模型应该如下所示:

img

映射模型

Xcode菜单中选择File ▸ New File,选择iOS ▸ Core Data ▸ Mapping Model模板。 选择版本3作为源模型,版本4作为目标模型。 将文件命名为 UnCloudNotesMappingModel_v3_to_v4

Xcode中打开新的映射模型,您将看到Xcode再次为您填充了一些映射。

NoteToNote映射开始,Xcode直接将源实体从源存储复制到目标,没有进行转换或转换。 这个简单的数据迁移的默认Xcode值很好用,就像现在一样!

选择AttachmentToAttachment映射。 Xcode还检测到了源和目标实体中的一些公共属性,并生成了映射。 但是,您希望将Attachment实体转换为ImageAttachment实体。 Xcode在这里创建的内容将把旧的Attachment实体映射到新的Attachment实体,这不是本次迁移的目标。 删除此映射。

接下来,选择ImageAttachment映射。 这个映射没有源实体,因为它是一个全新的实体。 在检查器中,将源实体更改为附件。 现在Xcode知道了源代码,它将为您填充一些值表达式。 Xcode还会将映射重命名为更合适的名称,AttachmentToImageAttachment

img

对于剩余的未填充属性,您需要编写一些代码。 这就是你需要图像处理和自定义代码的地方,而不仅仅是简单的FUNCTION表达式! 但首先,删除那些额外的映射,captionheightwidth。 这些值将使用自定义迁移策略进行计算,这恰好是下一节!

自定义迁移策略

要超越映射模型中的FUNCTION表达式,可以直接子类化NSEntityMigrationPolicy。 这让你可以编写Swift代码来处理迁移,一个实例接一个实例,这样你就可以调用应用其余部分可用的任何框架或库。

在项目中添加新的Swift文件,名为 AttachmentToImageAttachmentMigrationPolicyV3toV4.swift 并将其内容替换为以下启动代码:

import CoreData
import UIKit

let errorDomain = "Migration"

class AttachmentToImageAttachmentMigrationPolicyV3toV4:
  NSEntityMigrationPolicy {

}

这个命名约定您应该很熟悉;值得注意的是,这是一个自定义迁移策略,用于将数据从模型版本3中的Attachments转换为模型版本4中的ImageAttachments

您可能希望在忘记之前将这个新映射类连接到新创建的映射文件。返回v3v4映射模型文件,选择AttachmentToImageAttachment实体映射。 在Entity Mapping Inspector中,在 Custom Policy 字段中填写您刚刚创建的全命名空间类名(包括模块):

  • UnCloudNotes.AttachmentToImageAttachmentMigrationPolicyV3toV4

当您按Enter键确认此更改时,自定义策略上方的类型应更改为“自定义”。

Core Data运行此迁移时,它将在需要为该特定数据集执行数据迁移时创建自定义迁移策略的实例。 这是您运行任何自定义转换代码以在迁移期间提取图像信息的机会! 现在,是时候向自定义实体映射策略添加一些自定义逻辑了。

打开 AttachmentToImageAttachmentMigrationPolicyV3toV4.swift 并添加迁移方法:

override func createDestinationInstances(
  forSource sInstance: NSManagedObject,
  in mapping: NSEntityMapping,
  manager: NSMigrationManager
) throws {
  // 1
  let description = NSEntityDescription.entity(
    forEntityName: "ImageAttachment",
    in: manager.destinationContext)
    let newAttachment = ImageAttachment(
    entity: description!,
    insertInto: manager.destinationContext
  )

  // 2
  func traversePropertyMappings(
    block: (NSPropertyMapping, String) -> Void
  ) throws {
    if let attributeMappings = mapping.attributeMappings {
      for propertyMapping in attributeMappings {
        if let destinationName = propertyMapping.name {
          block(propertyMapping, destinationName)
        } else {
          // 3
          let message =
            "Attribute destination not configured properly"
          let userInfo =
            [NSLocalizedFailureReasonErrorKey: message]
          throw NSError(domain: errorDomain,
                        code: 0, userInfo: userInfo)
        }
      }
    } else {
      let message = "No Attribute Mappings found!"
      let userInfo = [NSLocalizedFailureReasonErrorKey: message]
      throw NSError(domain: errorDomain,
                    code: 0, userInfo: userInfo)
    }
  }

  // 4
  try traversePropertyMappings { propertyMapping, destinationName in
    if let valueExpression = propertyMapping.valueExpression {
      let context: NSMutableDictionary = ["source": sInstance]
      guard let destinationValue =
        valueExpression.expressionValue(
        with: sInstance,
        context: context
      ) else {
          return
      }

      newAttachment.setValue(destinationValue,
                             forKey: destinationName)
    }
  }

  // 5
  if let image = sInstance.value(forKey: "image") as? UIImage {
    newAttachment.setValue(image.size.width, forKey: "width")
    newAttachment.setValue(image.size.height, forKey: "height")
  }

  // 6
  let body = sInstance.value(
    forKeyPath: "note.body"
  ) as? NSString ?? ""
  newAttachment.setValue(
    body.substring(to: 80),
    forKey: "caption"
  )

  // 7
  manager.associate(
    sourceInstance: sInstance,
    withDestinationInstance: newAttachment,
    for: mapping
  )
}

此方法覆盖默认的NSEntityMigrationPolicy实现。 它是迁移管理器用来创建目标实体的实例。 源对象的实例是第一个参数;当被覆盖时,开发人员需要创建目标实例并将其正确地关联到迁移管理器。

下面是正在进行的一步一步:

  1. 首先,创建新目标对象的实例。 迁移管理器有两个Core Data堆栈-一个用于从源读取,另一个用于写入目标-因此您需要确保在这里使用目标上下文。 现在,您可能会注意到,本节没有使用新的花哨短ImageAttachment(context:NSManagedObjectContext)初始化器。 好吧,事实证明,使用新的语法,这个迁移将简单地崩溃,因为它依赖于已经加载和完成的模型,而这在迁移的中途还没有发生。
  2. 接下来,创建一个traversePropertyMappings函数,如果迁移中存在属性映射,该函数将执行迭代属性映射的任务。 该函数将控制遍历,而下一部分将执行每个属性映射所需的操作。
  3. 如果由于某种原因,entityMapping对象上的attributeMappings属性没有返回任何映射,这意味着您的映射文件指定不正确。 当这种情况发生时,该方法将抛出一个带有一些有用信息的错误。
  4. 尽管这是一个自定义的手动迁移,但是大多数属性迁移都应该使用您在映射模型中定义的表达式来执行。 为此,请使用上一步中的遍历函数,将值表达式应用于源实例,并将结果设置为新的目标对象。
  5. 接下来,尝试获取图像的实例。 如果它存在,获取其宽度和高度以填充新对象中的数据。
  6. 对于标题,只需抓取笔记的正文文本并获取前80个字符。
  7. 迁移管理器需要知道源对象、新创建的目标对象和映射之间的连接。 如果在自定义迁移结束时未能调用此方法,将导致目标存储区中缺少数据。

这就是自定义迁移代码! 当Core Data在启动时检测到v3数据存储时,它将选择映射模型,并将其应用于迁移到新的数据模型版本。 由于您添加了自定义的NSEntityMigrationPolicy子类并在映射模型中链接到它,因此Core Data将自动调用您的代码。

最后,是时候返回到主UI代码并更新数据模型用法,以考虑新的ImageAttachment实体。 打开 AttachPhotoViewController.swift,找到imagePickerController(_:didFinishPickingMediaWithInfo:)

更改设置附件的行,使其使用ImageAttachment

let attachment = ImageAttachment(context: context)

当你在这里的时候,你还应该向caption属性添加一个值。 caption属性是一个必需的字符串值,因此如果创建了一个ImageAttachment而没有值(即 一个nil值),则保存将失败。

理想情况下,应该有一个额外的字段来输入值,但现在添加以下行:

attachment.caption = "New Photo"

接下来,打开 Note.swift 并将image属性替换为以下内容:

var image: UIImage? {
  let imageAttachment = latestAttachment as? ImageAttachment
  return imageAttachment?.image
}

现在所有的代码更改都已经到位,您需要更新主数据模型,以使用v4作为主数据模型。

选择项目导航器中的UnCloudNotesDataModel.xcdatamodeld。 在“标识”窗格中,在Model Version下选择UnCloudNotesDataModel v4

构建并运行应用程序。数据应正确迁移。 同样,您的笔记将在那里,图像和所有,但您现在已经启用了未来的UnCloudNotes添加视频,音频和其他任何东西!

迁移非顺序版本

到目前为止,您已经按顺序完成了一系列数据迁移。 您已经按顺序将数据从版本1迁移到版本2、版本3和版本4。 不可避免地,在App Store发布的真实的世界中,用户可能会跳过更新,并需要从版本2到4。 然后呢?

Core Data执行迁移时,其目的是仅执行单个迁移。 在这个假设的场景中,Core Data将寻找从版本2到版本4的映射模型;如果不存在核心数据会推断出一个只要你告诉它 否则,迁移将失败,并且当尝试将存储附加到持久存储协调器时,Core Data将报告错误。

如何处理这种情况,使您请求的迁移成功? 您可以提供多个映射模型,但随着应用的增长,您需要提供大量的映射模型:从v1v4v1v3v2v4等等。 你会花更多的时间在映射模型上,而不是在应用程序本身!

解决方案是实现完全自定义的迁移序列。 您知道从版本2到版本3的迁移是有效的;从2迁移到4,如果您手动将存储从2迁移到3以及从3迁移到4,则效果良好。 这种逐步迁移意味着您将避免Core Data寻求直接的2到4或甚至1到4迁移。

自迁移栈

要开始实现这个解决方案,您需要创建一个单独的migration manager类。 该类的职责是在被要求时提供正确迁移的核心数据堆栈。 这个类将有一个stack属性,并将返回CoreDataStack的实例,正如UnCloudNotes在整个过程中使用的那样,它已经运行了对应用程序有用的所有必要的迁移。

首先,创建一个名为DataMigrationManager的新Swift文件。 打开文件并将其内容替换为以下内容:

import Foundation
import CoreData

class DataMigrationManager {
  let enableMigrations: Bool
  let modelName: String
  let storeName: String = "UnCloudNotesDataModel"
  var stack: CoreDataStack

  init(modelNamed: String, enableMigrations: Bool = false) {
    self.modelName = modelNamed
    self.enableMigrations = enableMigrations
  }
}

你会注意到,我们将开始看起来像当前的CoreDataStack初始化器。 这是为了使下一步更容易理解。

接下来打开 NotesListViewController.swift 并替换堆栈惰性初始化代码,如下图所示:

private lazy var stack: CoreDataStack =
  CoreDataStack(modelName: "UnCloudNotesDataModel")

With:

private lazy var stack: CoreDataStack = {
  let manager = DataMigrationManager(
    modelNamed: "UnCloudNotesDataModel",
    enableMigrations: true)
  return manager.stack
}()

你将使用lazy属性来保证堆栈只初始化一次。 其次,初始化实际上是由DataMigrationManager处理的,所以使用的堆栈将是从migration manager返回的堆栈。 如上所述,新的DataMigrationManager初始化器的签名类似于CoreDataStack。 这是因为有大量的迁移代码要写,将迁移的责任与保存数据的责任分开是一个好主意。

!!! 音符 由于您尚未初始化DataMigrationManager中的stack属性的值,因此该项目还不会构建。 放心吧,很快就到了。

现在进入最难的部分:如何判断存储是否需要迁移? 如果是这样,你怎么知道从哪里开始? 为了进行完全自定义的迁移,您将需要一些支持。 首先,找出模型是否匹配并不明显。 您还需要一种方法来检查持久存储文件与模型的兼容性。 让我们先从所有的支持功能开始吧!

DataMigrationManager.swift的底部,在NSManagedObjectModel上添加扩展:

extension NSManagedObjectModel {
  private class func modelURLs(
    in modelFolder: String
  ) -> [URL] {
    Bundle.main
      .urls(forResourcesWithExtension: "mom",
      subdirectory: "\(modelFolder).momd") ?? []
  }

  class func modelVersionsFor(
    modelNamed modelName: String
  ) -> [NSManagedObjectModel] {
    modelURLs(in: modelName)
      .compactMap(NSManagedObjectModel.init)
  }

  class func uncloudNotesModel(
    named modelName: String
  ) -> NSManagedObjectModel {
    let model = modelURLs(in: "UnCloudNotesDataModel")
      .first { $0.lastPathComponent == "\(modelName).mom" }
      .flatMap(NSManagedObjectModel.init)
    return model ?? NSManagedObjectModel()
  }
}

第一个方法返回给定名称的所有模型版本。 第二个方法返回名为UnCloudNotesDataModelNSManagedObjectModel的特定实例。 通常,Core Data会给予你最新的数据模型版本,但这种方法可以让你深入挖掘特定的版本。

Note

Xcode将你的应用编译到它的应用包中时,它也会编译你的数据模型。 应用程序包将在其根目录下有一个。包含momd的文件夹.mom的文件。 MOM 或托管对象模型文件是的编译版本.xcdatamodel文件。 你会有一个.mom用于每个数据模型版本。

要使用此方法,请在NSManagedObjectModel类扩展中添加以下方法:

class var version1: NSManagedObjectModel {
  uncloudNotesModel(named: "UnCloudNotesDataModel")
}

此方法返回数据模型的第一个版本。 这样可以获取模型,但是检查模型的版本呢? 将以下属性添加到类扩展:

var isVersion1: Bool {
  self == Self.version1
}

NSManagedObjectModel的比较运算符对于正确检查模型相等性没有多大帮助。 要使==比较在两个NSManagedObjectModel对象上起作用,请将以下运算符函数添加到文件中。 你需要在类扩展之外,在全局范围内添加这个:

func == (
  firstModel: NSManagedObjectModel,
  otherModel: NSManagedObjectModel
) -> Bool {
  firstModel.entitiesByName == otherModel.entitiesByName
}

这里的想法很简单:如果两个NSManagedObjectModel对象具有相同的实体集合,并且具有相同的版本哈希值,则它们是相同的。

现在一切都设置好了,您可以为接下来的3个版本重复versionisVersion模式。 继续并为版本2到4添加以下方法到类扩展:

class var version2: NSManagedObjectModel {
  uncloudNotesModel(named: "UnCloudNotesDataModel v2")
}

var isVersion2: Bool {
  self == Self.version2
}

class var version3: NSManagedObjectModel {
  uncloudNotesModel(named: "UnCloudNotesDataModel v3")
}

var isVersion3: Bool {
  self == Self.version3
}

class var version4: NSManagedObjectModel {
  uncloudNotesModel(named: "UnCloudNotesDataModel v4")
}

var isVersion4: Bool {
  self == Self.version4
}

现在您已经有了比较模型版本的方法,您将需要一种方法来检查特定的持久存储是否与模型版本兼容。 将这两个helper方法添加到DataMigrationManager类中:

private func store(
  at storeURL: URL,
  isCompatibleWithModel model: NSManagedObjectModel
) -> Bool {
  let storeMetadata = metadataForStoreAtURL(storeURL: storeURL)

  return model.isConfiguration(
    withName: nil,
    compatibleWithStoreMetadata:storeMetadata
  )
}

private func metadataForStoreAtURL(
  storeURL: URL
) -> [String: Any] {
  let metadata: [String: Any]
  do {
    metadata = try NSPersistentStoreCoordinator
    .metadataForPersistentStore(
      ofType: NSSQLiteStoreType,
      at: storeURL, 
      options: nil
    )
  } catch {
    metadata = [:]
    print("Error retrieving metadata for store at URL:
      \(storeURL): \(error)")
  }
  return metadata
}

第一种方法是一个简单的便利包装器,用于确定持久性存储是否与给定模型兼容。 第二种方法通过安全地检索存储的元数据来提供帮助。

接下来,将以下计算属性添加到DataMigrationManager类中:

private var applicationSupportURL: URL {
  let path = NSSearchPathForDirectoriesInDomains(
    .applicationSupportDirectory,
    .userDomainMask,
    true
  ).first
  return URL(fileURLWithPath: path!)
}

private lazy var storeURL: URL = {
  let storeFileName = "\(self.storeName).sqlite"
  return URL(
    fileURLWithPath: storeFileName,
    relativeTo: self.applicationSupportURL
  )
}()

private var storeModel: NSManagedObjectModel? {
  NSManagedObjectModel.modelVersionsFor(modelNamed: modelName)
    .first {
        self.store(at: storeURL, isCompatibleWithModel: $0)
    }
}

这些属性允许您访问当前存储URL和模型。 事实证明,CoreData API中没有方法向商店询问其模型版本。 相反,最简单的解决方案是暴力破解。 由于您已经创建了帮助器方法来检查商店是否与特定模型兼容,因此您只需要迭代所有可用的模型,直到找到与商店兼容的模型。

接下来,您需要迁移管理器记住当前模型版本。 要做到这一点,首先要创建一个通用的方法来从包中获取模型,然后使用这个通用的方法来查找模型。

首先,在NSManagedObjectModel类扩展中添加以下方法:

class func model(
  named modelName: String,
  in bundle: Bundle = .main
) -> NSManagedObjectModel {
  bundle
    .url(forResource: modelName, withExtension: "momd")
    .flatMap(NSManagedObjectModel.init)
      ?? NSManagedObjectModel()
}

这个方便的方法用于使用顶层文件夹初始化托管对象模型。 Core Data将自动查找当前模型版本,并将该模型加载到NSManagedObjectModel中使用。 需要注意的是,此方法仅适用于已版本化的Core Data模型。

接下来,在DataMigrationManager类中添加一个属性,如下所示:

private lazy var currentModel: NSManagedObjectModel =
    .model(named: self.modelName)

currentModel属性是惰性的,所以它只加载一次,因为它每次都应该返回相同的内容。 .model是调用刚添加的函数的简写方式,该函数将从顶层的momd文件夹中查找模型。

当然,如果您拥有的模型不是当前的模型,那就是运行迁移的时候了! 将以下starter方法添加到DataMigrationManager类中(稍后将填写):

func performMigration() {
}

接下来,将前面添加的堆栈属性定义替换为以下内容:

var stack: CoreDataStack {
  guard enableMigrations,
    !store(
      at: storeURL,
      isCompatibleWithModel: currentModel
    )
  else { return CoreDataStack(modelName: modelName) }

  performMigration()
  return CoreDataStack(modelName: modelName)
}

最后,computed属性将返回CoreDataStack实例。 如果设置了迁移标志,则检查初始化中指定的存储是否与Core Data确定为数据模型的当前版本兼容。 如果不能用当前模型加载存储,则需要对其进行迁移。 否则,您可以使用具有模型当前设置的任何版本的堆栈对象。

现在,您拥有了一个自迁移的Core Data堆栈,可以保证始终与最新的模型版本保持同步! 构建项目以确保所有内容都能编译。 下一步是添加自定义迁移逻辑。

自迁移栈

现在是开始构建迁移逻辑的时候了。 在DataMigrationManager类中添加以下方法:

private func migrateStoreAt(
  URL storeURL: URL,
  fromModel from: NSManagedObjectModel,
  toModel to: NSManagedObjectModel,
  mappingModel: NSMappingModel? = nil
) {
  // 1
  let migrationManager =
    NSMigrationManager(sourceModel: from, destinationModel: to)

  // 2
  var migrationMappingModel: NSMappingModel
  if let mappingModel = mappingModel {
    migrationMappingModel = mappingModel
  } else {
    migrationMappingModel = try! NSMappingModel
    .inferredMappingModel(
        forSourceModel: from, destinationModel: to)
  }

  // 3
  let targetURL = storeURL.deletingLastPathComponent()
  let destinationName = storeURL.lastPathComponent + "~1"
  let destinationURL = targetURL
    .appendingPathComponent(destinationName)

  print("From Model: \(from.entityVersionHashesByName)")
  print("To Model: \(to.entityVersionHashesByName)")
  print("Migrating store \(storeURL) to \(destinationURL)")
  print("Mapping model: \(String(describing: mappingModel))")

  // 4
  let success: Bool
  do {
    try migrationManager.migrateStore(
      from: storeURL,
      sourceType: NSSQLiteStoreType,
      options: nil,
      with: migrationMappingModel,
      toDestinationURL: destinationURL,
      destinationType: NSSQLiteStoreType,
      destinationOptions: nil
    )
    success = true
  } catch {
    success = false
    print("Migration failed: \(error)")
  }

  // 5
  if success {
    print("Migration Completed Successfully")

    let fileManager = FileManager.default
    do {
        try fileManager.removeItem(at: storeURL)
        try fileManager.moveItem(
          at: destinationURL,
          to: storeURL
        )
    } catch {
      print("Error migrating \(error)")
    }
  }
}

这种方法可以完成所有繁重的工作。 如果你需要做一个轻量级的迁移,你可以传递nil或者直接跳过最后一个参数。

下面是正在进行的一步一步:

  1. 首先,创建迁移管理器的实例。
  2. 如果一个映射模型被传递到了方法中,那么就使用它。 否则,创建推断映射模型。
  3. 由于迁移将创建第二个数据存储并逐个实例地将数据从原始文件迁移到新文件,因此目标URL必须是不同的文件。 现在,本节中的示例代码将创建一个destinationURL,它与原始文件和一个与“~1”连接的文件夹相同。 目标URL可以位于临时文件夹中,也可以位于您的应用有权写入文件的任何位置。
  4. 这里是您让迁移管理器工作的地方! 您已经使用源模型和目标模型设置了它,因此您只需要将映射模型和两个URL添加到组合中。
  5. 给出结果后,您可以将成功或错误消息打印到控制台。 在成功的情况下,您还执行了一点清理。 在这种情况下,删除旧商店并替换为新商店就足够了。

现在只需使用正确的参数调用该方法即可。 还记得performMigration的空实现吗? 是时候把它填进去了。

向该方法添加以下行:

if !currentModel.isVersion4 {
  fatalError("Can only handle migrations to version 4!")
}

这段代码将只检查当前模型是否是模型的最新版本。 如果当前模型不是版本4,则此代码会跳出并终止应用程序。 这有点极端-在您自己的应用程序中,您可能希望继续迁移-但如果您将另一个数据模型版本添加到您的应用程序中,以这种方式进行迁移肯定会提醒您考虑迁移! 值得庆幸的是,即使这是performMigration方法中的第一次检查,也不应该运行它,因为下一节在应用了最后一次可用的迁移后停止。

可以改进performMigration方法以处理所有已知的模型版本。 为此,在前面添加的if语句下面添加以下内容:

if let storeModel = self.storeModel {
  if storeModel.isVersion1 {
    let destinationModel = NSManagedObjectModel.version2

    migrateStoreAt(
      URL: storeURL,
      fromModel: storeModel,
      toModel: destinationModel
    )

    performMigration()
  } else if storeModel.isVersion2 {
    let destinationModel = NSManagedObjectModel.version3
    let mappingModel = NSMappingModel(
      from: nil,
      forSourceModel: storeModel,
      destinationModel: destinationModel
    )

    migrateStoreAt(
      URL: storeURL,
      fromModel: storeModel,
      toModel: destinationModel,
      mappingModel: mappingModel
    )

    performMigration()
  } else if storeModel.isVersion3 {
    let destinationModel = NSManagedObjectModel.version4
    let mappingModel = NSMappingModel(
      from: nil,
      forSourceModel: storeModel,
      destinationModel: destinationModel
    )

    migrateStoreAt(
      URL: storeURL,
      fromModel: storeModel,
      toModel: destinationModel,
      mappingModel: mappingModel
    )
  }
}

无论您从哪个版本开始,步骤都是类似的:

  • 轻量级迁移使用简单的标志来1)启用迁移,以及2)推断映射模型。 由于migrateStoreAt方法将在缺少映射模型时推断出一个映射模型,因此您已经成功地替换了该功能。 通过运行performMigration,您已经启用了迁移。
  • 将目标模型设置为正确的模型版本。 记住,你一次只能“升级”一个版本,所以从1到2,从2到3。
  • 对于版本2和更高版本,还要加载映射模型。
  • 最后,调用migrateStoreAt(URL:fromModel:toModel:mappingModel:),这是您在本节开始时编写的。

这个解决方案的优点是,尽管有所有的比较支持帮助函数,DataMigrationManager类本质上使用了已经为每个迁移定义的映射模型和代码。

此解决方案是按顺序手动应用每个迁移,而不是让Core Data尝试自动执行操作。

Note

如果您从版本1或2开始,则在最后会递归调用performMigration()。 这将触发另一次运行以继续该序列;一旦您处于版本3并运行迁移以获得版本4。 在添加更多数据模型版本以继续自动迁移序列时,您将继续添加到此方法。

测试顺序迁移

测试这种类型的迁移可能有点复杂,因为您需要回到过去并运行应用程序的早期版本来生成要迁移的数据。 如果您在开发过程中保存了应用程序项目的副本,那就太好了!

否则,您将在本书附带的资源中找到该项目的以前版本。

首先,确保你复制了一个项目,因为它是现在的-这是最终的项目!

以下是测试每次迁移所需的一般步骤:

  1. 从模拟器中删除应用程序以清除数据存储。
  2. 打开应用程序的版本2(这样你至少可以看到一些图片!), 建造和运行
  3. 创建一些测试笔记。
  4. Xcode退出应用程序并关闭项目。
  5. 打开应用程序的最终版本,然后构建并运行。

此时,您应该看到一些带有迁移状态的控制台输出。 请注意,迁移将在应用程序显示在屏幕上之前进行。

img

您现在拥有了一个应用程序,该应用程序将在旧数据版本的任何组合之间成功迁移到最新版本。

关键点

  • 当您需要对数据模型进行更改时,迁移是必要的。
  • 尽可能使用最简单的迁移方法。
  • 轻量级迁移是Apple的术语,用于迁移时您所涉及的工作量最少。
  • 正如Apple所描述的,重量级迁移可以包含几种不同类型的自定义迁移。
  • 自定义迁移允许您创建一个映射模型来指导Core Data进行轻量级无法自动完成的更复杂的更改。
  • 一旦创建了映射模型,就不要更改目标模型。
  • 自定义手动迁移比映射模型更进一步,允许您从代码更改模型。
  • 完全手动迁移允许您的应用从一个版本顺序迁移到下一个版本,防止用户跳过将其设备更新到中间版本时出现问题。
  • 迁移测试很棘手,因为它依赖于源存储区中的数据。 在将应用发布到App Store之前,请务必测试多个场景。