Core Data 数据迁移拾遗

综合技术 2018-02-02 阅读原文

首先感谢掘金猫D 的推荐,很多朋友关注了我,由于刚开始写技术文章,并没有写出什么真正的干货,深感愧疚。

今天讨论的是 Core Data 数据迁移中的一些细节问题,参阅了不少资料,进行了反复的验证,可以说填上了不少坑。

本文讨论的范围仅限于 自动触发的自定义迁移
情况,其他情况后续再补充。

数据备份与还原

在迁移测试前,先将原数据进行备份:

  • 打开 Xcode -> Windows -> Devices ...
  • 选择要备份的 App,点击⚙️小图标,选择 Download container
  • SQLite 文件在此目录下: xxx.xcappdata/AppData/Library/Application Support/xxx.sqlite
  • 如果需要还原数据,在 Xcode 相同的菜单下,选择 Replace Container

新建数据模型版本

  • 选择数据模型文件 xxx.xcdatamodeld
    ,打开菜单 Editor -> Add Model Version
    ,根据提示添加新版本。
  • 在 Xcode 右侧文件属性中,选择 Model Version
    为新建的版本。
  • 在新的模型文件中编辑需要的改动。如果如前面所属,两个版本在形式上一致(记录、属性、类型都相同),但是实质又不同,为了能自动触发迁移,需要在有差异的实体或属性上,在 Xcode 面板中找到 Versioning -> Hash Modifier
    ,填写任意的名称,只有这样运行时才会认为两个版本不同。如果有形式上的差异,这个字段可以不用填写。
  • 如果新版本的模型文件不符合要求,想要删除重建时,在 Xcode 中无法直接删除新版本,此时可以在 Finder 中直接操作,通过右键 显示包内容
    进入内部,删除对应的版本文件。但此时文件还是会存在 Xcode 中且以红色显示,打开工程目录下的 project.pbxproj
    ,按模型文件名搜索,删除对应的行即可。

建立映射模型

  • 要进行自定义迁移,必须要有映射模型,它的作用就是告诉 App 老版数据怎么转移到新版模型里去。这一步一定要在新的数据模型版本最终确定后,再来操作,否则如果不一致运行时就会报错提示找不到映射模型。如果发现模型版本要回去改,那么最好是删除并重新创建映射模型。

  • 模型中有任何变动,包括修改 Hash Modifier
    等,也可以通过刷新的方式更新 Mapping Model,选中映射模型文件,打开菜单 Editor -> Refresh Data Models
    ,这个时候会发现 Xcode 中值都变空了,右键点击映射模型文件 Open As -> Quick Look
    ,再点击一次 Open As -> Mapping Model
    ,这时候就刷新显示了。

  • 有一种情况也会造成报错找不到映射模型,就是已安装的 App 中的 SQlite 文件会记录模型版本的哈希值,这个值与当前运行的模型版本计算出的值是不一致的,造成这个的原因就是一定修改过了模型版本。凡是在 Xcode 编辑器中对模型文件做了任何修改,包括上述的 Hash Modifier
    等等,都会造成哈希值不同,App 就会认为发现了一个新的模型文件版本,但现有的映射模型是不匹配的。此时只能恢复模型文件与 App 中安装的版本保持一致。如果确实要做这些修改,只能老实地再新建一个模型版本,以及新的映射模型。

  • 映射模型中的 Value Expression
    ,实际上是 NSExpression
    类型值,因此要按照 NSExpression
    的规则来写。它可以进行简单的数学运算(数字类型属性),如 $source.xxx + 10
    ;也可以使用类似于 KVC 中的 KeyPath,如 $source.xxx.yyy
    ,但使用 KeyPath 的方式要注意, xxx
    必须为 NSObject 的子类,在 Swift 中使用需注意,另外如果 xxx
    为集合类型,还可以使用集合操作符,使用方式参考 KVC 的集合操作。

    对于 yyy
    的类型有一个要注意,如果要映射的 xxx
    属性类型是 Data
    Transformable
    类型,并且实际存储的是自定义类的情况,那么 yyy
    只能引用 xxx
    中的存储属性或实例方法(不带参数,带参数的见下一条 FUNCTION
    使用),如果直接引用计算属性则会出错(因为实际存储中并没有这个值),但引用计算属性的 get
    方法是可以的,如有一个计算属性是 property
    ,那么应该再提供一个 getProperty()
    的方法,再使用 $source.xxx.getProperty
    引用。

  • 属性映射还有一个方法就是使用 FUNCTION(object, selector, parameter...)
    ,类似 objc_msgSend
    语法,其中 object
    代表迁移过程中可以使用到的对象,例如以下几个都是 Core Data 预设的 key, selector
    代表 object
    拥有的方法指针, parameter
    为具体参数:

// Core Data 预设的 key
NSMigrationManagerKey: $manager
NSMigrationSourceObjectKey: $source
NSMigrationDestinationObjectKey: $destination
NSMigrationEntityMappingKey: $entityMapping
NSMigrationPropertyMappingKey: $propertyMapping
NSMigrationEntityPolicyKey: $entityPolicy

其中 selector
的写法需要非常注意,在 swift 中如果你的方法是 combine(firstName:String, lastName: String),那么在 FUNCTION 中就要写成 combineWithFirstName:lastName:
,中间要加 "With",Objc 中应该也是类似。如果第一个参数名是 with
from
(第一个字母均为小写),或者方法名以 With
From
结尾(注意第一个字母要大写),那就不用再加 With
,如果不是这种命名方式,编译时程序就会在第一个参数名前自动加上 With
进行匹配。如果不确定怎么写方法名,可以在 playground 中打印出来,如:

class Test {
    @objc func combine(firstName: String, lastName: String) -> String {
        return firstName + lastName
    }
}

print(#selector(Test.combine(firstName:lastName:))) // combineWithFirstName:lastName:
  • 如果以上写法都无法完成映射转换,那就要自定义迁移策略了。

自定义迁移策略

  • 这个策略其实只针对映射过程,在 Xcode 编辑器中无法满足映射需求时,需要自定义迁移策略类。
  • 创建 NSEntityMigrationPolicy
    的子类,通过重写类中的方法,实现自定义映射,如以下代码完成目标对象的映射过程,所有未在代码中自定义的映射都会在映射模型中去找,所以只写非直接复制的部分:
final class V1To2Policy: NSEntityMigrationPolicy {
    override func createDestinationInstances(forSource sInstance: NSManagedObject, in mapping: NSEntityMapping, manager: NSMigrationManager) throws
    {
        try super.createDestinationInstances(
            forSource: sInstance, in: mapping, manager: manager)
        guard let xxx = sInstance.value(forKey: "xxx") else { return } // 获取原始属性值
        let newValue = .... // 计算映射后的属性值
        guard let newItem = manager.destinationInstances( // 获取映射后的新对象
            forEntityMappingName: "XXXToXXX", // 注意这里要与映射模型中的实体映射名称一致
            sourceInstances: [sInstance]).first else { return }
        newItem.setValue(newValue, forKey: "xxx") // 设置新对象的属性值
    }
}
  • 最后将定义完的迁移策略,填写在映射模型中实体映射的 Custom Policy
    字段中,策略名称一定要按照这个规范填写: ModuleName.CustomPolicyClassName
    ,例如你要运行的 Target 名称为 ExampleApp
    ,自定义策略类名为 CoreDataModelV1ToV2
    ,那么最终就填写 ExampleApp.CoreDataModelV1ToV2
    。另外还有一个特例,如果 Target 名称以数字开头,如 1ExampleApp
    ,实际应该填写(1改为下划线): _ExampleApp.CoreDataModelV1ToV2
    这个可能是 Xcode 自动做的处理。不确定 ModuleName
    的,随便找个 storyboard 或 xib 文件查看源码,找到 customModule
    字段里是什么值,这个就代表你的 Target ModuleName

托管对象子类

  • 数据模型变化之后,托管对象子类也需要进行相应改动,这里只需要按照最新的模型版本改动即可。但在原始模型的实体属性中,或者迁移策略中,如果使用到了原有的类型或方法,注意保留,否则读取原始数据或迁移时将报类型错误(这也是产生多余兼容代码的一个方面,应尽量避免这样的设计,例如对于实体属性中 Data、Transformable 类型,尽量不要直接存储自定义类型,而应存储基本类型及其组合,再通过方法转换到自定义类型上。)。

迁移选项设置

  • 迁移标志: NSPersistentContainer
    有一个属性 persistentStoreDescriptions
    ,或者 NSPersistentStoreCoordinator
    addPersistentStore
    方法中有一个 options
    选项,在此选项中添加 Migration Options
// iOS 10 及以上写法
container.persistentStoreDescriptions[0].shouldMigrateStoreAutomatically = true
container.persistentStoreDescriptions[0].shouldInferMappingModelAutomatically = false

// iOS 10 以下写法
let options = [NSMigratePersistentStoresAutomaticallyOption: true,
               NSInferMappingModelAutomaticallyOption: false]
do {
    try coordinator.addPersistentStore(ofType: NSSQLiteStoreType, configurationName: nil, at: storeURL, options: options)
} catch {
    fatalError("Failed to add persistent store: (error)")
}

Core Data 还有很多说不完的话题,慢慢来吧。

欢迎访问我的个人网站 ,阅读更多文章。

题图: Zigzag - la_paupiette_masquee @unsplash

稀土掘金

责编内容by:稀土掘金阅读原文】。感谢您的支持!

您可能感兴趣的

iOS: 如何正确的绘制1像素的线 iOS 绘制1像素的线 一、Point Vs Pixel iOS中当我们使用Quartz,UIKit,CoreAnimation等框架时,所有的坐标系统采用Point来衡量。系统在实际渲染到设置时会帮助我们处理Point到Pixel的转换。这样做的好处隔离变化,即我们在布局的事后不需要关...
Rejected due to background location mode my app update (1.1) was rejected because of this reason: We found that your app uses a background mode but does not include functionality that req...
Gather the state of the iPhone 3g Is it possible to find the state in which the user is with MapKit of the iPhone? I am using the MapKit for the iPhone and have i...
Debugging iOS Applications: A Guide to Debug Other... A cheat sheet to debug third-party iOS applications quickly. Since everyone loves a good cheat sheet, and there is not one readily available for deb...
Adjust the width of UILabel according to its conte... I had a look on SO before asking this question. All the questions are about adjusting the height of a UILabel and not its width. I tried alternati...