14、Realm数据库迁移:从基础到高级实践

Realm数据库迁移:从基础到高级实践

1. 迁移基础与线性/非线性迁移

在应用开发过程中,随着版本的迭代,数据库的架构也会发生变化。模拟运行三个应用版本及其成功迁移后的输出,展示了在应用新版本发布时,如何应用自定义逻辑来更新现有用户架构和数据。

线性迁移通过多次检查旧版本来实现,示例代码如下:

if oldVersion < 2 { 
  migrateFrom1To2(migration) 
} 

if oldVersion < 3 { 
  migrateFrom2to3(migration) 
}

这种逐个检查版本并分别执行迁移步骤的方式,允许在应用的非连续版本之间执行组合迁移步骤。例如,如果用户依次安装每个版本的应用,通常只会执行完整迁移序列的最后一步。但如果用户跳过某个版本,迁移将执行更多步骤以弥补跳过的迁移。

以下是一个简单的流程图展示线性迁移过程:

graph TD;
    A[开始] --> B{旧版本 < 2};
    B -- 是 --> C[迁移到版本2];
    B -- 否 --> D{旧版本 < 3};
    C --> D;
    D -- 是 --> E[迁移到版本3];
    D -- 否 --> F[结束];
    E --> F;
2. 已知问题与关键点

在当前版本的Realm中,迁移存在一些已知的限制和问题:
| 问题 | 描述 |
| ---- | ---- |
| 降低架构版本 | 不支持降低架构版本,尝试指定低于磁盘上的架构版本会抛出错误。 |
| 新增属性默认值 | 新增属性不会应用默认值,例如添加 dynamic var title = "Default" ,现有对象的 title 值将为空字符串。 |
| 新增主键 | 迁移期间新增的主键不会反映在新架构中,该属性仅被标记为索引,迁移完成并打开文件后,主键才会生效。 |
| 访问基本类型列表 | 迁移期间无法访问基本类型列表,Realm会错误地将列表转换为 List<DynamicObject> 。 |

迁移的关键点包括:
- 移动应用可能会与数据库架构失去同步,因此需要对对象架构进行版本控制。
- 大多数对象架构的更改会由Realm自动迁移,如添加或删除对象和属性等简单更改。
- 非平凡的更改以及自定义数据导入或处理需要在迁移块中执行,该块在应用用户升级到最新版本后调用。

3. 挑战:添加新应用版本

挑战是在Playground中添加一个新的应用版本2.5,将对象架构版本增加到4,并从架构中移除 Mark 对象。可以使用 Migration 类的适当方法删除现有Realm文件中关于 Mark 的所有元信息。

4. 高级架构迁移项目准备

在Xcode项目中进行更复杂的迁移,该项目可构建同一应用的三个不同版本,允许编写代码并模拟真实的应用升级。
- 项目初始化 :打开项目文件夹,运行 pod install 安装依赖,然后在Xcode中打开项目工作区。
- 版本选择 :Xcode窗口顶部的Scheme选择器可选择不同版本的应用,每个Scheme定义了编译时标志,用于启用或禁用项目中的不同代码块。
- 文件跟踪 AppVersions 文件夹包含每个版本更改的文件,主要有以下几类:
- EntitiesX.X.swift :包含每个应用版本使用的Realm对象。
- MigratorX.X.swift :包含跨版本演变的迁移代码。
- ExamCellViewModelX.X.swift :用于在屏幕上显示考试信息的视图模型。
- NavigatorX.X.swift :应用的中央导航器类。

5. 应用各版本详细分析
5.1 Exams 1.0

选择 Exams 1.0 Scheme并运行项目,该版本默认创建两个考试,列表显示考试名称和学分。查看 AppVersions/1.0/Entities1.0.swift 可检查该版本的Realm架构。

5.2 Exams 1.5

此版本有两个改进:用户可以标记考试为通过,以及区分多项选择题考试。
- 更新应用代码
- 打开 AppVersions/1.5/Entities1.5.swift ,在 Exam 类中添加两个新属性:

enum Property: String { 
  case date, subject, result, isMultipleChoice 
}
dynamic var result: String? 
dynamic var isMultipleChoice = false
  • 打开 AppVersions/1.5/ExamCellViewModel1.5.swift 进行如下更改:
    • 添加新属性 let exam: Exam
    • init(exam:) guard 语句后插入 self.exam = exam
    • 替换 title = subject.name 为:
title = (exam.isMultipleChoice ? "☒ ": "") 
  .appending(subject.name) 
  .appending(exam.result != nil ? "(\(exam.result!))": "")
- 在 `guard` 语句内添加 `self.exam = exam` 以解决编译错误。
- 替换 `setExamsSelected` 方法内容:
guard exam.result == nil,  
      let realm = exam.realm else { return false } 

try! realm.write { 
  exam.result = "I’ve passed the exam!" 
} 

return true
  • 添加迁移代码
    打开 AppVersions/1.5/Migrator1.5.swift ,在 migrate(migration:oldVersion:) 方法中添加:
if oldVersion < 2 { 
  print("Migrate to Realm Schema 2") 

  let multipleChoiceText = " (multiple choice)" 
  enum Key: String { case isMultipleChoice, subject, name } 
  var migratedExamCount = 0 

  migration.enumerateObjects( 
    ofType: String(describing: Exam.self)) { _, newExam in 
      guard let newExam = newExam, 
        let subject = newExam[Key.subject.rawValue] as? MigrationObject, 
        let subjectName = subject[Key.name.rawValue] as? String  
        else { return } 
      if subjectName.contains(multipleChoiceText) { 
        newExam[Key.isMultipleChoice.rawValue] = true 
        subject[Key.name.rawValue] = subjectName 
          .replacingOccurrences(of: multipleChoiceText, with: "") 
      } 
      migratedExamCount += 1 
    } 

  print("Schema 2: Migrated \(migratedExamCount) exams")
}
  • 运行迁移 :选择版本1.5的应用并运行项目,控制台应显示迁移信息,UI应反映架构更改。
5.3 Exams 2.0

此版本将替换 Exam 类的自由格式 result 属性,使用单独的Realm模型存储每个考试的结果。
- 更新应用代码
- 打开 AppVersions/2.0/Entities2.0.swift ,将 dynamic var result: String? 替换为 dynamic var passed: Result? ,并更新 Exam.Property 枚举。
- 添加 Result 类:

@objcMembers final class Result: Object { 
  enum Property: String { case result } 
  enum Value: String { case notSet = "Not set", pass, fail } 

  dynamic var result = "" 

  override static func primaryKey() -> String? { 
    return Result.Property.result.rawValue 
  } 
  static func initialData() -> [[String: String]] { 
    return [Value.notSet, Value.pass, Value.fail] 
      .map { [Property.result.rawValue: $0.rawValue] } 
  } 
}
  • 添加 MigrationTask 类用于持久化未完成的自定义交互式迁移任务。
  • 打开 AppVersions/2.0/ExamCellViewModel2.0.swift ,替换相关代码:
.appending(exam.passed != nil ? " (\(exam.passed!.result))": "")
  • 添加迁移代码
    打开 AppVersion/2.0/Migrator2.0.swift ,在 migrate(migration:oldVersion:) 方法底部添加:
if oldVersion < 3 { 
  enum Key: String { case result, passed } 
  var isInteractiveMigrationNeeded = false 
  let results = Result.initialData().map { 
    return migration.create(String(describing: Result.self), value: $0) 
  } 
  let noResult = results.first! 
  let examClassName = String(describing: Exam.self) 

  migration.enumerateObjects( 
    ofType: examClassName) { oldExam, newExam in 
      guard let oldExam = oldExam, let newExam = newExam else { return } 
      if schema(migration.oldSchema, 
        includesProperty: Key.result.rawValue, for: examClassName), 
        oldExam[Key.result.rawValue] as? String != nil { 
          isInteractiveMigrationNeeded = true 
      } else { 
        newExam[Key.passed.rawValue] = noResult 
      } 
    } 
  if isInteractiveMigrationNeeded { 
    MigrationTask.create(task: .askForExamResults, in: migration) 
  } 
}
  • 添加交互式迁移
    打开 AppVersion/2.0/Navigator2.0.swift ,添加新的类扩展:
extension Navigator { 
  func navigateToMigrationExamStatus(_ ctr: UINavigationController) { 
    let migrateExamsVC = MigrateExamStatusViewController 
      .createWith { [weak self] in 
        self?.navigateToMainScene() 
      } 
    ctr.setViewControllers([migrateExamsVC], animated: true) 
  } 
}

navigateToMainScene() 方法中添加检查待处理迁移任务的代码:

if let migrationTask = MigrationTask.first(in: RealmProvider.exams.realm) 
{ 
  switch migrationTask.name { 
  case MigrationTask.TaskType.askForExamResults.rawValue: 
    navigateToMigrationExamStatus(controller) 
    return 
  default: break 
  } 
  return 
}
  • 运行迁移 :确保在运行版本1.5时至少有一个考试有自定义结果,然后运行版本2.0,将看到自定义迁移视图控制器,依次完成交互式迁移后,点击 Done 按钮将呈现主场景。

通过以上步骤,我们详细介绍了Realm数据库迁移的基础、高级实践以及遇到的问题和解决方案,希望能帮助开发者更好地处理数据库架构的变化。

Realm数据库迁移:从基础到高级实践

6. 交互式迁移的详细流程与注意事项

在Exams 2.0版本中引入的交互式迁移是一个较为复杂但重要的功能,下面详细阐述其流程和需要注意的方面。

6.1 交互式迁移的触发条件

交互式迁移的触发条件是当存在考试有自由格式的文本结果时。在迁移代码中,通过检查旧架构中 Exam 对象的 result 属性是否有值来判断是否需要交互式迁移:

if schema(migration.oldSchema,
  includesProperty: Key.result.rawValue, for: examClassName),
  oldExam[Key.result.rawValue] as? String != nil {
    isInteractiveMigrationNeeded = true
}

isInteractiveMigrationNeeded 被设置为 true 时,会创建一个 MigrationTask 对象,标记需要进行交互式迁移:

if isInteractiveMigrationNeeded {
  MigrationTask.create(task: .askForExamResults, in: migration)
}
6.2 交互式迁移的执行流程

当应用启动时,会在 Navigator 类的 navigateToMainScene() 方法中检查是否存在待处理的迁移任务:

if let migrationTask = MigrationTask.first(in: RealmProvider.exams.realm)
{
  switch migrationTask.name {
  case MigrationTask.TaskType.askForExamResults.rawValue:
    navigateToMigrationExamStatus(controller)
    return
  default: break
  }
  return
}

如果存在 askForExamResults 类型的迁移任务,会调用 navigateToMigrationExamStatus(_:) 方法,展示自定义的迁移视图控制器 MigrateExamStatusViewController

extension Navigator {
  func navigateToMigrationExamStatus(_ ctr: UINavigationController) {
    let migrateExamsVC = MigrateExamStatusViewController
      .createWith { [weak self] in
        self?.navigateToMainScene()
      }
    ctr.setViewControllers([migrateExamsVC], animated: true)
  }
}

用户在该视图控制器中,需要为每个有自由格式结果的考试选择一个预定义的结果选项(”Fail”, “Pass” 或 “Not Set”)。完成所有选择后,点击 Done 按钮,视图模型会完成迁移任务并调用完成回调,呈现主场景。

6.3 交互式迁移的注意事项
  • 作为最后手段 :交互式迁移应该是在其他迁移步骤都无法完成数据迁移时的最后选择。因为它会增加应用非线迁移的设计和编码难度。
  • 任务持久化 :使用 MigrationTask 类将未完成的迁移任务持久化到 Realm 文件中,确保即使应用在迁移过程中被杀死,下次启动时仍能继续进行迁移。
  • 任务顺序 MigrationTask.first(in:) 方法可以对迁移任务进行排序,例如按优先级排序,确保任务按合理的顺序执行。

以下是交互式迁移的流程图:

graph TD;
    A[应用启动] --> B{是否有迁移任务};
    B -- 是 --> C{任务类型是否为 askForExamResults};
    C -- 是 --> D[展示迁移视图控制器];
    D --> E[用户选择结果];
    E --> F[点击 Done 完成迁移];
    F --> G[呈现主场景];
    C -- 否 --> H[其他处理];
    B -- 否 --> G;
7. 迁移总结与最佳实践

通过对不同版本应用的迁移实践,我们可以总结出一些迁移的最佳实践:

7.1 版本控制
  • 对对象架构进行版本控制是非常必要的,因为移动应用可能会与数据库架构失去同步。在迁移代码中,通过检查旧版本号来决定执行哪些迁移步骤,例如:
if oldVersion < 2 {
  migrateFrom1To2(migration)
}

if oldVersion < 3 {
  migrateFrom2to3(migration)
}
7.2 自动迁移与自定义迁移
  • 大多数简单的架构更改,如添加或删除对象和属性,Realm 会自动处理。但对于非平凡的更改和自定义数据导入或处理,需要在迁移块中编写自定义代码。
  • 在迁移块中,使用 Migration 类的方法来操作数据,如 enumerateObjects(ofType:_:) 遍历对象, create(_:value:) 创建新对象等。
7.3 交互式迁移的使用
  • 交互式迁移应谨慎使用,仅在无法通过代码自动完成迁移时使用。在使用时,要确保迁移任务的持久化和合理的任务顺序。
7.4 代码组织
  • 将迁移代码组织在 Migrator 类中,每个版本的迁移代码可以独立维护。同时,使用枚举来定义迁移过程中需要访问的键,避免直接使用属性名,提高代码的可维护性。

以下是一个总结表格:
| 最佳实践 | 描述 |
| ---- | ---- |
| 版本控制 | 对对象架构进行版本控制,通过检查旧版本号执行迁移步骤 |
| 自动迁移与自定义迁移 | 简单更改由 Realm 自动处理,复杂更改编写自定义迁移代码 |
| 交互式迁移的使用 | 作为最后手段,确保任务持久化和合理顺序 |
| 代码组织 | 将迁移代码组织在 Migrator 类中,使用枚举定义键 |

通过遵循这些最佳实践,开发者可以更高效地处理 Realm 数据库的迁移问题,确保应用在版本迭代过程中数据的完整性和一致性。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值