Core Data by tutorials 笔记(四)

Raywenderlich 家《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,命名为 UnCloudNotesMappingModelv2to_v3
  3. 修改 mapping model,分为 Attribute MappingsRelationship Mappings

    上图是实体 Notemapping model,这里的 source 指的是源数据模型(data model)里的 Note 实体,创建新加实体 Attachmentmapping model 也很简单,在 Entity Mapping inspector 里将 source entity 改为 Note,接着实体 Attachment 的属性 dateCreated、image 就来自于上一版 data model 里的 Note 实体。

    在 Mapping model 中可以添加过滤条件,比如设置 NoteToAttachment 的 Filter Predicate 为 image != nil,也就是说 Attachment 的迁移只有在 image 存在的情况下发生。

  4. Relationship mapping,这里要注意的一点就是实体 Note 与 Attachment 的关系是在 UnCloudNotesDataModel v3 这一版本中添加的,所以我们需要的 destination relationship 其实就是 UnCloudNotesDataModel v3 中的 relationship。于是我们这样获得这段关系

    作者这里展示了这个表达式函数:

    FUNCTION($manager,"destinationInstancesForEntityMappingNamed:sourceInstances:","NoteToNote", $source)
    
  5. 最后需要更改之前CoreData的 options 设置:将自动推断 mapping model 关掉,因为我们已经自定义了 mapping model。

五、A complex mapping model

  1. 创建一个 UnCloudNotesDataModel v4 的版本,在 v3 的版本上增加一个 Entity,命名为 ImageAttachment,设为 Attachment 的子类。接着为这个新的 ImageAttachment 添加 caption、width、height 三个属性,移除 Attachment 中的 image。这样就为今后支持videos、audio 做好了扩展准备。
  2. 添加 UnCloudNotesMappingModelv3to_v4,和上一节类似, NoteToNote mappingAttachmentToAttachment mapping ,Xcode 已经为我们设置 OK 了,我们只需关注 AttachmentToImageAttachment,修改他的 $source 为 Attachment
    除了从父类 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 InspectorCustom 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. 添加两个计算属性,storeURLstoreModel,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版本。

作者最后还给了两条建议:

  • 尽量可能采取最简单的迁移方式,因为迁移很难测试。
  • 每个版本都尽量保存一点数据以便将来迁移时可以测试。

-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!