💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-si Raywenderlich家[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)这本书到此为止已经回顾过半,今天来学习一下第六章“版本迁移”。第六章也是本书篇幅最多的。根据数据模型的每一次的调整程度,数据迁移都有可能会变得更加复杂。最后,迁移数据所花的成本甚至超过了所要实现的功能。那么前期完善对Model的设计将会变得十分重要,这一切都需要开发者去权衡。 ## **Chapter 6: Versioning and Migration** 本章提供了一个记事本APP,未来数据结构要变更,迁移(migration)过程就是:在旧data model的基础上将数据迁移到新的data model中来。 ### **一、When to migrate** 如果仅仅是把Core data当做是离线缓存用,那么下次update的时候,丢弃掉就OK了。但是,如果是需要保存用户的数据,在下个版本仍然能用,那么就需要迁移数据了,具体操作是创建一个**新版本的data model**,然后提供一个*迁移路径(migration path)*。 ### **二、The migration process** 在创建Core Data stack的时候,系统会在添加store到persistent store coordinator之前分析这个store的model版本,接着与coordinator中的data model相比较,如果不匹配,那么Core Data就会执行迁移。当然,你要启用*允许迁移*的选项,否则会报错。 具体的迁移需要源data model和目的model,根据这两个版本的model创建mapping model,mapping model可以看做是迁移所需要的地图。 迁移主要分三步: 1. Core Data拷贝所有的对象从一个data store到另一个。 2. Core Data根据**relationship mapping**重建所有对象的关系 3. 在destination model开启数据有效性验证,在此之前的copy过程中是被disable了。 这里不用担心出错,Core Data只有迁移成功,才会删除原始的data store数据。 作者根据日常经验将迁移划分为四种: * Lightweight migrations * Manual migrations * Manual migrations * Fully manual migrations ~~~ 第一种是苹果的方式,你几乎不用做什么操作,打开选项迁移就会自动执行。第二种需要设置一个mapping model类似与data model,也是全GUI操作没什么难度。第三种,就需要你在第二种的基础上自定义迁移策略(NSEntityMigrationPolicy)供mapping model选择。最后一种考虑的是如何在多个model版本中跨版本迁移,你要提供相应的判定代码。 ~~~ ### **三、A lightweight migration** 所谓轻量级的迁移就是给Note实体增加了一个image的属性。要做的步骤也很简单: 1. 在上一model基础上创建UnCloudNotesDataModel v2,然后添加image属性。 2. 启用Core Data自动迁移选项,这个选项在**.addPersistentStoreWithType方法**中开启 > 作者的做法是在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比作是向导。紧接着列出了可以应用自动推断的一些模式,基本上都是对实体、属性的增、删、改以及关系的修改。 > 1. Deleting entities, attributes or relationships; > 2. Renaming entities, attributes or relationships using the renamingIdentifier; > 3. Adding a new, optional attribute; > 4. Adding a new, required attribute with a default value; > 5. Changing an optional attribute to non-optional and specifying a default value; > 6. Changing a non-optional attribute to optional; > 7. Changing the entity hierarchy; > 8. Adding a new parent entity and moving attributes up or down the hierarchy; > 9. Changing a relationship from to-one to to-many; > 10. Changing a relationship from non-ordered to-many to ordered to-many (and vice versa). 所以正确的做法就是任何数据迁移都应先从自动迁移开始,如果搞不定才需要手动迁移。 ### **四、A manual migration** 1. 与lightweight migration相同,首先要创建一个UnCloudNotesDataModel v3,这次需要添加一个新Entity,命名为Attachment,并给该Entity添加两个属性dateCreated、image。将Note和Attachment的关系设为一对多,即一个note会有多个attachment。 2. 创建一个**mapping model**,命名为UnCloudNotesMappingModel_v2_to_v3 3. 修改mapping model,分为**Attribute Mappings**和**Relationship Mappings** ![](https://box.kancloud.cn/2015-08-21_55d6eafb36e8f.jpg) 上图是实体**Note**的**mapping model**,这里的source指的是源数据模型(data model)里的Note实体,创建新加实体**Attachment**的*mapping model*也很简单,在Entity Mapping inspector里将**source entity**改为**Note**,接着实体Attachment的属性dateCreated、image就来自于上一版data model里的Note实体。 ![](https://box.kancloud.cn/2015-08-21_55d6eb010a342.jpg) 在Mapping model中可以添加过滤条件,比如设置NoteToAttachment的**Filter Predicate**为image != nil,也就是说Attachment的迁移只有在image存在的情况下发生。 4. Relationship mapping,这里要注意的一点就是实体Note与Attachment的关系是在UnCloudNotesDataModel v3这一版本中添加的,所以我们需要的*destination relationship*其实就是UnCloudNotesDataModel v3中的relationship。于是我们这样获得这段关系 ![](https://box.kancloud.cn/2015-08-21_55d6eb0123eca.jpg) 作者这里展示了这个表达式函数: ~~~ FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:", "NoteToNote", $source) ~~~ 5. 最后需要更改之前CoreData的*options*设置 ~~~ options:[NSMigratePersistentStoresAutomaticallyOption:true, NSInferMappingModelAutomaticallyOption:false] ~~~ 将自动推断mapping model关掉,因为我们已经自定义了mapping model。 ### **五、A complex mapping model** 1. 创建一个UnCloudNotesDataModel v4的版本,在v3的版本上增加一个Entity,命名为**ImageAttachment**,设为*Attachment*的子类。接着为这个新的ImageAttachment添加caption、width、height三个属性,移除Attachment中的image。这样就为今后支持videos、audio做好了扩展准备。 2. 添加UnCloudNotesMappingModel_v3_to_v4,和上一节类似,**NoteToNote mapping**和**AttachmentToAttachment mapping**Xcode已经为我们设置OK了,我们只需关注**AttachmentToImageAttachment**,修改他的$source为**Attachment** ![](https://box.kancloud.cn/2015-08-21_55d6eb0158727.jpg) 除了从父类*Attachment*继承而来的属性,新添加的三个属性都没有mapping,我们用代码来实现吧。 3. 除了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*。 ## **六、Migrating non-sequential versions** 如果存在多个版本非线性迁移,也就是可能从V1直接到V3或V4...这又该怎么办呢,这节代码比较多,说下思路,就不全帖出来了。 1. 创建一个DataMigrationManager,这个类有一个stack属性,由他来负责提供合适的migrated Core Data stack。为了分清各个版本,这个manager初始化需要传入store name和model name两个参数。 2. 扩展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. 3. 根据上面扩展的方法,继续对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) } ~~~ 4. 增加store和model是否匹配的判断方法,这里主要用NSPersistentStoreCoordinator的**metadataForPersistentStoreOfType方法**返回一个**metadata**,然后再用model的**isConfiguration方法**对这个*metadata*进行判断,来决定model和persistent store是否匹配。 5. 添加两个计算属性,**storeURL**和**storeModel**,storeModel遍历所有的model,通过第4步的判断方法找出相匹配的storeModel。 6. 修改stack的定义:先判断,store与model不相容,就先执行迁移。 ~~~ var stack: CoreDataStack { if !storeIsCompatibleWith(Model: currentModel) { performMigration() } return CoreDataStack(modelName: modelName, storeName: storeName, options: options) } ~~~ 7. 自定义一个迁移方法,将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) { //...... } ~~~ 8. 最后我们来实现第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版本。 作者最后还给了两条建议: * 尽量可能采取最简单的迁移方式,因为迁移很难测试。 * 每个版本都尽量保存一点数据以便将来迁移时可以测试。