轻量级 Core Data 迁移教程

原文:Lightweight Migrations in Core Data Tutorial
作者:Saul Mora
译者:kmyhy

在编写 Core Data app的时候,你会为 app 指定一个初始数据模型。但是,在你发布 app 之后,数据模型免不了需要进行一些修改。那该怎么办?你不想让现有的用户受到影响!

你无法预见未来,通过 Core Data,你可以在每个 app 发布新版时向后迁移。这个迁移过程会将使用老数据模型的数据进行升级,以般配当前的数据模型。

这篇 Core Data 迁移教程从各个方面讨论 Core Data 迁移,通过一个记事本 app 的版本演变为例子。你会从一个简单的 app 开始,在它的数据模型中只有一个实体。

让我们开始这个伟大的迁移吧!

注意:这个教程假设你具备基本的 Core Data 和 Swift 技能。如果你是 Swift 新手,请参考我们的 Swift 教程:快速入门
如果你是 Core Data 新手,请参考这篇:Your First Core Data App Using Swift tutorial

什么时候需要迁移

什么是需要进行一次迁移?最简单的答案是“就是你需要修改数据模型的时候”。

但是,仍然有一些情况可以避免不必要的迁移。如果 app 中仅仅是将 Core Data 作为离线缓存使用,那么当你升级 app 的时候,可以简单地删除和重建数据库。这只能使用在你明确知道用户数据没有保存在数据库的情况下。否则,你需要保护你的用户数据。

也就是说,如果不能够在不修改数据模型情况下实现对设计或功能进行修改,那么你就无法避免要创建新的 data model 版本并提供一种迁移方式。

迁移的过程

在初始化 Core Data 時,有一个步骤是将 store 添加到持久化 store coordinator 中。在这一步骤中,Core Data 在将 store 添加到 store coordinator 之前会做几个工作。

首先,Core Data 会分析 store 中模型的版本。然后将之和 coordinator 中设置的 数据模型版本进行比较。如果 store 中的模型的版本和 coordinator 中的模型版本不匹配,Core Data 会尝试进行迁移动作。

注意:如果不允许迁移,而且 store 和模型无法兼容,Core Data 不会将 store 添加到 coordinator,并返回一个包含特定错误码的错误。

要进行迁移,Core Data 需要拥有原来的数据模型和目标数据模型。为了能够迁移,它必须使用这两个版本创建出一个映射模型。映射模型用于将原来的数据库中的数据转换为能够存入新数据库中的数据。只有 计算出映射模型,最终才能进行迁移。

迁移分成 3 个步骤:

  • 首先,Core Data 从一个库中将数据全部复制到另一个库。
  • 根据关系映射,Core Data 创建对象之间的关系。
  • 最后,Core Data 根据目标模型进行数据校验。在数据拷贝期间,Core Data 关闭了目标模型的数据校验。

你可能会问,“如果发生错误,源数据库上会发生什么?”无论是哪种迁移类型,除非迁移完全成功,否则源数据库不会发生任何改变。只有迁移成功,Core Data才会删除源数据库。

迁移类型

以我个人的经验,在轻量级迁移和重量级迁移之间还有几种迁移类型。下面列出几种迁移类型,虽然这些类型没有得到官方的承认。这将从最简单的迁移类型开始列出,直到最复杂的迁移类型。
Types of Migrations

轻量级迁移

轻量级迁移是苹果的说法,这种迁移你需要做的工作最少。当你使用 NSPersistentContainer时,迁移是自动完成的,最多需要在编译你的 Core Data 时设置一些标志。这种方式对于数据模型的改变有一些限制,但因为所需的工作量最少,这是一种理想的设置。

手动迁移

手动迁移需要你做更多的工作。你需要手动指定如何将老的数据集映射成新的数据集,但这种方式的好处是,你可以指定一个显式的映射模型文件并在文件中进行配置。在 Xcode 中创建一个映射模型文件和创建数据模型类似, UI 界面和操作过程类似。

定制手动迁移

这是第三复杂的迁移方式。你可以使用映射模型,但通过代码来指定自定义的数据转换逻辑。自定义实体转换逻辑需要创建一个 NSEntityMigrationPolicy 子类,在这个类中执行自定义的转换。

完全手动迁移

当使用自定义转换逻辑都无法将数据完全从一个版本迁移到另一个版本时,就只能使用完全手动迁移了。自己定制版本检查逻辑和自行定制迁移过程。

开始

从这里下载开始项目

在模拟器中运行项目。你会看到一个空白的列表:

点击右上角的 + 按钮添加一条新的备忘录。添加一个标题(为了简便起见,备忘录的正文使用默认填充的文本),然后点击 Create 按钮,将数据保存到数据库。重复添加,以便数据库中有一些数据能够迁移。

返回 Xcode,打开 UnCloudNotesDatamodel.xcdatamodeld 文件,打开 Xcode 的 实体建模工具。数据模型非常简单 —— 只有一个实体 Note,它有几个属性:

在这个 App中我们将添加一个新特性:能够将一张照片添加到备忘录中去。现在数据模型中没有任何地方能够保存这个信息,因此你需要在数据模型中添加一个地方来保存照片。但在 app 中我们已经有几条数据了。你能够在不删除已有数据的情况下修改数据模型吗?

首先来尝试第一种迁移!

注意:比起之前的版本, Xcode8 控制台中会输出更多的的内容。解决办法是加一个 OS_ACTIVITY_MODE=disable 到当前 scheme的环境变量中。

轻量级迁移

在 Xcode中,选择 UnCloudNotes 数据模型文件。这回在工作区中显示实体模型编辑器。然后,打开 Editor 菜单,选择 Add Model Version…。命名新版本为 UnCloudNotesDataModel v2 ,确保 Base on model 字段中选中的是 UnCloudNotesDataModel。Xcode 会创建一个 UnCloudNotesDataModel 的拷贝。

注意:你可以随意指定文件名。以 v2、v3、v4 等序号命名有助于表明文件的版本。

以上步骤将创建 data model的第二个版本。但你还需要告诉 Xcode,使用新版本的模型作为当前模型。如果你不进行这个步骤,而是选择了最上面的 UnCloudNotesDataModel.xcdatamodeld 文件 的话,则所有的修改都会针对老模型进行。你可以针对单个模型进行版本的选择,但你仍然需要确保不会意外修改到老的模型文件。

为了使迁移能够进行,你需要确保源模型文件丝毫不动,所有的修改都是针对新模型文件的。

在文件检查模板中,底部有一个选项叫做 Model Version。将它修改为新版本的数据模型 UnCloudNotesDataModel v2:

改好之后,注意项目导航窗口中的绿色的小勾,这个小勾从原来的数据模型移到了 v2 版的数据模型:

在 Core Data 初始化内存时,总是试图第一个连接带勾的模型的版本所对应数据库。如果找到数据库文件,数据库和模型文件不兼容,就会触发迁移。旧版本是为了迁移而存在的。当前模型会被 Core Data 事先加载到内存以便你使用。

确保 v2 版的数据模型已选中,然后给 Note 实体添加一个 image属性。属性名设置为image,类型为 Transformable。

因为这个属性会存储图片的二进制数据,你可以用一个自定义的 NSValueTransformer 将二进制数据转换成 UIImage,或者反过来转换。在 ImageTransformer 中已经为你准备好了一个类似的转换器。在右边数据模型检查器的 Value Transformer Name 字段中,输入 ImageTransformer,Module 字段输入 UnCloudNotes。

注意:就像在 Xib 和故事板文件中一样,当你在模型文件中引用一个源文件时,你必须指定一个模块(在本例中也就是 UnCloudNotes),这样类加载器才能找到你想使用的源文件。

现在新的模型已经可以在代码中使用了!打开 Note.swift,在 displayIndex 下面添加一个属性:

@NSManaged var image: UIImage?

编译和运行程序。你会看到你的备忘录仍然奇迹般地列出来了。这是因为默认情况下轻量级迁移是开启的。每当你创建一个新的数据模型版本时,自动迁移就会执行。这实在太省事了!

映射模型推断

当你在 NSPersistentStoreDescription 上打开 shouldInferMappingModelAutomatically 选项时,Core Data 在许多情况下都能够推断出映射模型。Core Data 会自动比较两个数据模型之间的异同,并在二者之间创建一个映射模型。

如果两个模型版本中的实体和属性相同,直接通过映射复制数据。否则,Core Data 依照下面的简单规则创建映射模型。

在新模型中,修改必须按照明显的迁移模式进行,比如:

  • 删除了实体、属性或者关系
  • 通过 renamingIdentifier 修改了实体、属性或关系的名称
  • 增加了一个新的、optional 的属性
  • 增加了一个新的、带默认值的 required 属性
  • 将一个 optional 属性修改成非 optional,并提供了一个默认值
  • 将非 optional 属性修改成 optional 属性
  • 修改了实体的层级结构
  • 增加了新的父实体,并将层级结构中的属性上移/下移
  • 将关系从 to-one 修改为 to-many
  • 将关系从无序的 to-many 修改为有序的 to-many(或者反过来)

注意:关于 Core Data 如何推断轻量级迁移映射的详细介绍,请参考苹果文档: https://developer.apple.com/library/Mac/DOCUMENTATION/Cocoa/Conceptual/CoreDataVersioning/Articles/vmLightweightMigration.html

如上面所列举的, Core Data 能够判断两个数据模型之间的各种改变,更加难得的是,它能够自动根据不同的情况采取动作。第一定律是,如果有可能,所有的迁移都应当从轻量级迁移开始,只有在必要的时候才采用更高级的映射。

以从 UnCloudNotes 到 UnCloudNotes v2 的迁移为例,image 属性有一个默认值 nil,因为它是 optional的。因为这种改变符合上面的第三条轻量级迁移模式,所以 Core Data 能够轻松地将老数据库迁移到新数据库。

加上图片

现在数据已经迁移好,你需要修改界面,以便图片能够和备忘录关联上。幸运的是,这个工作的大部分都已经为你完成了。

打开 Main.storyboard 找到 Create Note 窗口。在下方,你会找到 Create Note With Images 窗口,这个窗口提供了添加图片的 UI。

Create Note 窗口是一个导航控制器的根视图控制器。右键从导航控制器拖一条线到 Create Note With Images 窗口,然后选择 root view Controller。

这将用新窗口替代老的 Create Note 窗口作为导航控制器的根控制器。


image6

接着打开 AttachPhotoViewController.swift 在 UIImagePickerControllerDelegate 扩展中加入方法:

func imagePickerController(_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [String: Any]) {

    guard let note = note else { return }

    note.image =
info[UIImagePickerControllerOriginalImage] as? UIImage

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

这将用新的 image 属性来保存用户从 Image Picker 中选定的图片。

然后,打开 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
}

根据 note 的 image 属性是否为空,从缓存中 dequeue 对应的 UITableViewCell 子类实例。最后打开 NoteImageTableViewCell.swift 在 updateNoteInfo(note:) 方法中加入:

noteImage.image = note.image

这会在 NoteImageTableViewCell 内部用 note 的 image 属性显示到 UIImageView 上。

编译运行,新建一个备忘录:

点击 Attach Image按钮,添加一张图片。从模拟器的照片库中选择一张图片:

App 使用标准的 UIImagePickerController 来添加图片并保存到备忘录中。

注:如果想将自己的图片添加到模拟器的相册中,打开模拟器窗口,拖一张图片文件进去。幸运的是,iOS 10 模拟器已经为你准备好了一个照片库。

如果使用真机,打开 AttachPhotoViewController.swift,将 Image Picker 的 sourceType 属性修改为 .camera,你就可以使用设备相机了。现有的代码是用的相册,因为模拟器是没有相机的。

结尾

最终完成的项目在此处下载。

后续的更高级的迁移教程请参考 Core Data by Tutorial 一书中的关于Core Data 迁移的内容。在书中的完整章节中,你会学到如何创建一个映射模型,将实体和属性从老版本迁移到新版本。你还会学到如何定制迁移策略,如何迁移非常规的数据模型。

我希望你喜欢这篇 Core Data迁移教程,如果有问题或建议,请留言!

如果你喜欢这篇教程,也许会喜欢这本Core Data by Tutorials

这本书会更加详细地介绍 Core Data,它为已经掌握基本的 iOS和Swift 技能,但想学习如何在 App 中利用 Core Data 存储数据的中级 iOS 开发者准备的。

它已经完全升级到了 Swift 3、iOS10 和 Xcode 8——立即到 raywenderlich.com 商店中去看看吧!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值