codecamp

Core Data by tutorials 笔记(四)

原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-si

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,命名为UnCloudNotesMappingModel_v2_to_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设置

    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 mappingAttachmentToAttachment mappingXcode已经为我们设置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版本。

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

  • 尽量可能采取最简单的迁移方式,因为迁移很难测试。
  • 每个版本都尽量保存一点数据以便将来迁移时可以测试。
Core Data by tutorials 笔记(三)
Core Data by tutorials 笔记(五)
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }