首先感谢掘金 猫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
字段里是什么值,这个就代表你的 TargetModuleName
。
托管对象子类
- 数据模型变化之后,托管对象子类也需要进行相应改动,这里只需要按照最新的模型版本改动即可。但在原始模型的实体属性中,或者迁移策略中,如果使用到了原有的类型或方法,注意保留,否则读取原始数据或迁移时将报类型错误(这也是产生多余兼容代码的一个方面,应尽量避免这样的设计,例如对于实体属性中 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 还有很多说不完的话题,慢慢来吧。
欢迎访问 我的个人网站 ,阅读更多文章。