原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-si
Raywenderlich家《Core Data by Tutorials》这本书到此为止已经回顾过半,今天来学习一下第六章“版本迁移”。第六章也是本书篇幅最多的。根据数据模型的每一次的调整程度,数据迁移都有可能会变得更加复杂。最后,迁移数据所花的成本甚至超过了所要实现的功能。那么前期完善对Model的设计将会变得十分重要,这一切都需要开发者去权衡。
本章提供了一个记事本APP,未来数据结构要变更,迁移(migration)过程就是:在旧data model的基础上将数据迁移到新的data model中来。
如果仅仅是把Core data当做是离线缓存用,那么下次update的时候,丢弃掉就OK了。但是,如果是需要保存用户的数据,在下个版本仍然能用,那么就需要迁移数据了,具体操作是创建一个新版本的data model,然后提供一个迁移路径(migration path)。
在创建Core Data stack的时候,系统会在添加store到persistent store coordinator之前分析这个store的model版本,接着与coordinator中的data model相比较,如果不匹配,那么Core Data就会执行迁移。当然,你要启用允许迁移的选项,否则会报错。
具体的迁移需要源data model和目的model,根据这两个版本的model创建mapping model,mapping model可以看做是迁移所需要的地图。
迁移主要分三步:
这里不用担心出错,Core Data只有迁移成功,才会删除原始的data store数据。
作者根据日常经验将迁移划分为四种:
Fully manual migrations
第一种是苹果的方式,你几乎不用做什么操作,打开选项迁移就会自动执行。第二种需要设置一个mapping model类似与data model,也是全GUI操作没什么难度。第三种,就需要你在第二种的基础上自定义迁移策略(NSEntityMigrationPolicy)供mapping model选择。最后一种考虑的是如何在多个model版本中跨版本迁移,你要提供相应的判定代码。
所谓轻量级的迁移就是给Note实体增加了一个image的属性。要做的步骤也很简单:
作者的做法是在CoreDataStack初始化的时候传入这个options数组参数,然后再传递给.addPersistentStoreWithType方法。
init(modelName: String, storeName: String,
options: NSDictionary? = nil) {
self.modelName = modelName
self.storeName = storeName
self.options = options
}
store = coordinator.addPersistentStoreWithType(
NSSQLiteStoreType, configuration: nil,
URL: storeURL,
options: self.options,
error: nil)
lazy var stack : CoreDataStack = CoreDataStack(
modelName:"UnCloudNotesDataModel",
storeName:"UnCloudNotes",
options:[NSMigratePersistentStoresAutomaticallyOption: true,
NSInferMappingModelAutomaticallyOption: true])
NSMigratePersistentStoresAutomaticallyOption是自动迁移选项,而NSInferMappingModelAutomaticallyOption是mapping model自动推断。所有的迁移都需要mapping model,作者也把mapping model比作是向导。紧接着列出了可以应用自动推断的一些模式,基本上都是对实体、属性的增、删、改以及关系的修改。
- Deleting entities, attributes or relationships;
- Renaming entities, attributes or relationships using the renamingIdentifier;
- Adding a new, optional attribute;
- Adding a new, required attribute with a default value;
- Changing an optional attribute to non-optional and specifying a default value;
- Changing a non-optional attribute to optional;
- Changing the entity hierarchy;
- Adding a new parent entity and moving attributes up or down the hierarchy;
- Changing a relationship from to-one to to-many;
- Changing a relationship from non-ordered to-many to ordered to-many (and vice versa).
所以正确的做法就是任何数据迁移都应先从自动迁移开始,如果搞不定才需要手动迁移。
修改mapping model,分为Attribute Mappings和Relationship Mappings
上图是实体Note的mapping model,这里的source指的是源数据模型(data model)里的Note实体,创建新加实体Attachment的mapping model也很简单,在Entity Mapping inspector里将source entity改为Note,接着实体Attachment的属性dateCreated、image就来自于上一版data model里的Note实体。
在Mapping model中可以添加过滤条件,比如设置NoteToAttachment的Filter Predicate为image != nil,也就是说Attachment的迁移只有在image存在的情况下发生。
Relationship mapping,这里要注意的一点就是实体Note与Attachment的关系是在UnCloudNotesDataModel v3这一版本中添加的,所以我们需要的destination relationship其实就是UnCloudNotesDataModel v3中的relationship。于是我们这样获得这段关系
作者这里展示了这个表达式函数:
FUNCTION($manager,
"destinationInstancesForEntityMappingNamed:sourceInstances:",
"NoteToNote", $source)
最后需要更改之前CoreData的options设置
options:[NSMigratePersistentStoresAutomaticallyOption:true,
NSInferMappingModelAutomaticallyOption:false]
将自动推断mapping model关掉,因为我们已经自定义了mapping model。
添加UnCloudNotesMappingModel_v3_to_v4,和上一节类似,NoteToNote mapping和AttachmentToAttachment mappingXcode已经为我们设置OK了,我们只需关注AttachmentToImageAttachment,修改他的$source为Attachment
除了从父类Attachment继承而来的属性,新添加的三个属性都没有mapping,我们用代码来实现吧。
除了mapping model中的FUNCTION expressions,我们还可以自定义migration policies。增加一个NSEntityMigrationPolicy类的swift文件命名为AttachmentToImageAttachmentMigrationPolicyV3toV4,覆盖NSEntityMigrationPolicy初始化方法:
class AttachmentToImageAttachmentMigrationPolicyV3toV4: NSEntityMigrationPolicy {
override func createDestinationInstancesForSourceInstance( sInstance: NSManagedObject,
entityMapping mapping: NSEntityMapping,
manager: NSMigrationManager, error: NSErrorPointer) -> Bool {
// 1 创建一个新destination object
let newAttachment = NSEntityDescription.insertNewObjectForEntityForName("ImageAttachment",
inManagedObjectContext: manager.destinationContext) as NSManagedObject
// 2 在执行手动migration之前,先执行mapping model里定义的expressions
for propertyMapping in mapping.attributeMappings as [NSPropertyMapping]! {
let destinationName = propertyMapping.name!
if let valueExpression = propertyMapping.valueExpression {
let context: NSMutableDictionary = ["source": sInstance]
let destinationValue: AnyObject = valueExpression.expressionValueWithObject(sInstance,
context: context)
newAttachment.setValue(destinationValue, forKey: destinationName)
}
}
// 3 从这里开始才是custom migration,从源object得到image的size
if let image = sInstance.valueForKey("image") as? UIImage {
newAttachment.setValue(image.size.width, forKey: "width")
newAttachment.setValue(image.size.height, forKey: "height")
}
// 4 得到caption
let body = sInstance.valueForKeyPath("note.body") as NSString
newAttachment.setValue(body.substringToIndex(80), forKey: "caption")
// 5 manager作为迁移管家需要知道source、destination与mapping
manager.associateSourceInstance(sInstance, withDestinationInstance:
newAttachment, forEntityMapping: mapping)
// 6 成功了别忘了返回一个bool值
return true
}
}
这样就定义了一个自定义迁移policy,最后别忘了在AttachmentToImageAttachment的Entity Mapping Inspector里Custom Policy那一栏填入我们上面创建的这个UnCloudNotes.AttachmentToImageAttachmentMigrationPolicyV3toV4。
如果存在多个版本非线性迁移,也就是可能从V1直接到V3或V4...这又该怎么办呢,这节代码比较多,说下思路,就不全帖出来了。
扩展NSManagedObjectModel,创建两个类方法:
class func modelVersionsForName(name: String) -> [NSManagedObjectModel]
class func uncloudNotesModelNamed(name: String) -> NSManagedObjectModel
前者根据model名称返回所有版本的model,后者返回一个指定的Model实例。
When Xcode compiles your app into its app bundle, it will also compile your data models. The app bundle will have at its root a .momd folder that contains .mom files. MOM or Managed Object Model files are the compiled versions of .xcdatamodel files. You’ll have a .mom for each data model version.
根据上面扩展的方法,继续对NSManagedObjectModel进行扩展,创建几个比较版本的handle method,例如:
class func version2() -> NSManagedObjectModel {
return uncloudNotesModelNamed("UnCloudNotesDataModel v2")
}
func isVersion2() -> Bool {
return self == self.dynamicType.version2()
}
直接使用“==”比较当然是不行的,这里继续对“==”改写一下,有同样的entities就判定相等:
func ==(firstModel:NSManagedObjectModel, otherModel:NSManagedObjectModel) -> Bool {
let myEntities = firstModel.entitiesByName as NSDictionary
let otherEntities = otherModel.entitiesByName as NSDictionary
return myEntities.isEqualToDictionary(otherEntities)
}
增加store和model是否匹配的判断方法,这里主要用NSPersistentStoreCoordinator的metadataForPersistentStoreOfType方法返回一个metadata,然后再用model的isConfiguration方法对这个metadata进行判断,来决定model和persistent store是否匹配。
添加两个计算属性,storeURL和storeModel,storeModel遍历所有的model,通过第4步的判断方法找出相匹配的storeModel。
修改stack的定义:先判断,store与model不相容,就先执行迁移。
var stack: CoreDataStack {
if !storeIsCompatibleWith(Model: currentModel) {
performMigration()
}
return CoreDataStack(modelName: modelName, storeName: storeName, options: options)
}
自定义一个迁移方法,将store URL、source model、destination model和可选的mapping model作为参数,这就是完全手动实现迁移的方法。如果做轻量级的迁移,将最后一个mapping model设为nil,那么使用本方法和系统实现没有差别。
func migrateStoreAt(URL storeURL:NSURL,
fromModel from:NSManagedObjectModel,
toModel to:NSManagedObjectModel,
mappingModel:NSMappingModel? = nil) {
//......
}
最后我们来实现第6步提到的performMigration方法,现在最新的版本是v4,开始之前先做个判断,当前model的最新版本为v4,才执行这个performMigration方法下面的内容:
if !currentModel.isVersion4() {
fatalError("Can only handle migrations to version 4!")
}
这样就变成了从v1 -> v4,v2 -> v4,v3 -> v4的迁移,接下来的方法也很简单,分别判断storeModle的版本号,执行第7步的migrateStoreAt:方法,并且通过对performMigration方法的递归调用来最终迁移到v4版本。
作者最后还给了两条建议: