Core Data by tutorials 筆記(四)

2018-02-24 15:54 更新

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

Raywenderlich家《Core Data by Tutorials》這本書到此為止已經(jīng)回顧過半,今天來學(xué)習(xí)一下第六章“版本遷移”。第六章也是本書篇幅最多的。根據(jù)數(shù)據(jù)模型的每一次的調(diào)整程度,數(shù)據(jù)遷移都有可能會變得更加復(fù)雜。最后,遷移數(shù)據(jù)所花的成本甚至超過了所要實現(xiàn)的功能。那么前期完善對Model的設(shè)計將會變得十分重要,這一切都需要開發(fā)者去權(quán)衡。

Chapter 6: Versioning and Migration

本章提供了一個記事本APP,未來數(shù)據(jù)結(jié)構(gòu)要變更,遷移(migration)過程就是:在舊data model的基礎(chǔ)上將數(shù)據(jù)遷移到新的data model中來。

一、When to migrate

如果僅僅是把Core data當(dāng)做是離線緩存用,那么下次update的時候,丟棄掉就OK了。但是,如果是需要保存用戶的數(shù)據(jù),在下個版本仍然能用,那么就需要遷移數(shù)據(jù)了,具體操作是創(chuàng)建一個新版本的data model,然后提供一個遷移路徑(migration path)。

二、The migration process

在創(chuàng)建Core Data stack的時候,系統(tǒng)會在添加store到persistent store coordinator之前分析這個store的model版本,接著與coordinator中的data model相比較,如果不匹配,那么Core Data就會執(zhí)行遷移。當(dāng)然,你要啟用允許遷移的選項,否則會報錯。
具體的遷移需要源data model和目的model,根據(jù)這兩個版本的model創(chuàng)建mapping model,mapping model可以看做是遷移所需要的地圖。
遷移主要分三步:

  1. Core Data拷貝所有的對象從一個data store到另一個。
  2. Core Data根據(jù)relationship mapping重建所有對象的關(guān)系
  3. 在destination model開啟數(shù)據(jù)有效性驗證,在此之前的copy過程中是被disable了。

這里不用擔(dān)心出錯,Core Data只有遷移成功,才會刪除原始的data store數(shù)據(jù)。

作者根據(jù)日常經(jīng)驗將遷移劃分為四種:

  • Lightweight migrations
  • Manual migrations
  • Manual migrations
  • Fully manual migrations

    第一種是蘋果的方式,你幾乎不用做什么操作,打開選項遷移就會自動執(zhí)行。第二種需要設(shè)置一個mapping model類似與data model,也是全GUI操作沒什么難度。第三種,就需要你在第二種的基礎(chǔ)上自定義遷移策略(NSEntityMigrationPolicy)供mapping model選擇。最后一種考慮的是如何在多個model版本中跨版本遷移,你要提供相應(yīng)的判定代碼。

三、A lightweight migration

所謂輕量級的遷移就是給Note實體增加了一個image的屬性。要做的步驟也很簡單:

  1. 在上一model基礎(chǔ)上創(chuàng)建UnCloudNotesDataModel v2,然后添加image屬性。
  2. 啟用Core Data自動遷移選項,這個選項在.addPersistentStoreWithType方法中開啟

作者的做法是在CoreDataStack初始化的時候傳入這個options數(shù)組參數(shù),然后再傳遞給.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比作是向?qū)?。緊接著列出了可以應(yīng)用自動推斷的一些模式,基本上都是對實體、屬性的增、刪、改以及關(guān)系的修改。

  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).

所以正確的做法就是任何數(shù)據(jù)遷移都應(yīng)先從自動遷移開始,如果搞不定才需要手動遷移。

四、A manual migration

  1. 與lightweight migration相同,首先要創(chuàng)建一個UnCloudNotesDataModel v3,這次需要添加一個新Entity,命名為Attachment,并給該Entity添加兩個屬性dateCreated、image。將Note和Attachment的關(guān)系設(shè)為一對多,即一個note會有多個attachment。
  2. 創(chuàng)建一個mapping model,命名為UnCloudNotesMappingModel_v2_to_v3
  3. 修改mapping model,分為Attribute MappingsRelationship Mappings

    上圖是實體Notemapping model,這里的source指的是源數(shù)據(jù)模型(data model)里的Note實體,創(chuàng)建新加實體Attachmentmapping model也很簡單,在Entity Mapping inspector里將source entity改為Note,接著實體Attachment的屬性dateCreated、image就來自于上一版data model里的Note實體。

    在Mapping model中可以添加過濾條件,比如設(shè)置NoteToAttachment的Filter Predicate為image != nil,也就是說Attachment的遷移只有在image存在的情況下發(fā)生。

  4. Relationship mapping,這里要注意的一點就是實體Note與Attachment的關(guān)系是在UnCloudNotesDataModel v3這一版本中添加的,所以我們需要的destination relationship其實就是UnCloudNotesDataModel v3中的relationship。于是我們這樣獲得這段關(guān)系

    作者這里展示了這個表達式函數(shù):

    FUNCTION($manager,
        "destinationInstancesForEntityMappingNamed:sourceInstances:",
        "NoteToNote", $source)
  5. 最后需要更改之前CoreData的options設(shè)置

    options:[NSMigratePersistentStoresAutomaticallyOption:true,
        NSInferMappingModelAutomaticallyOption:false]

    將自動推斷mapping model關(guān)掉,因為我們已經(jīng)自定義了mapping model。

五、A complex mapping model

  1. 創(chuàng)建一個UnCloudNotesDataModel v4的版本,在v3的版本上增加一個Entity,命名為ImageAttachment,設(shè)為Attachment的子類。接著為這個新的ImageAttachment添加caption、width、height三個屬性,移除Attachment中的image。這樣就為今后支持videos、audio做好了擴展準(zhǔn)備。
  2. 添加UnCloudNotesMappingModel_v3_to_v4,和上一節(jié)類似,NoteToNote mappingAttachmentToAttachment mappingXcode已經(jīng)為我們設(shè)置OK了,我們只需關(guān)注AttachmentToImageAttachment,修改他的$source為Attachment

    除了從父類Attachment繼承而來的屬性,新添加的三個屬性都沒有mapping,我們用代碼來實現(xiàn)吧。

  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 創(chuàng)建一個新destination object
            let newAttachment = NSEntityDescription.insertNewObjectForEntityForName("ImageAttachment",
                inManagedObjectContext: manager.destinationContext) as NSManagedObject
        // 2 在執(zhí)行手動migration之前,先執(zhí)行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那一欄填入我們上面創(chuàng)建的這個UnCloudNotes.AttachmentToImageAttachmentMigrationPolicyV3toV4。

六、Migrating non-sequential versions

如果存在多個版本非線性遷移,也就是可能從V1直接到V3或V4...這又該怎么辦呢,這節(jié)代碼比較多,說下思路,就不全帖出來了。

  1. 創(chuàng)建一個DataMigrationManager,這個類有一個stack屬性,由他來負責(zé)提供合適的migrated Core Data stack。為了分清各個版本,這個manager初始化需要傳入store name和model name兩個參數(shù)。
  2. 擴展NSManagedObjectModel,創(chuàng)建兩個類方法:

        class func modelVersionsForName(name: String) -> [NSManagedObjectModel]
    class func uncloudNotesModelNamed(name: String) -> NSManagedObjectModel

    前者根據(jù)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. 根據(jù)上面擴展的方法,繼續(xù)對NSManagedObjectModel進行擴展,創(chuàng)建幾個比較版本的handle method,例如:

    class func version2() -> NSManagedObjectModel {
        return uncloudNotesModelNamed("UnCloudNotesDataModel v2")
    }
    func isVersion2() -> Bool {
        return self == self.dynamicType.version2()
    }

    直接使用“==”比較當(dāng)然是不行的,這里繼續(xù)對“==”改寫一下,有同樣的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不相容,就先執(zhí)行遷移。

    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作為參數(shù),這就是完全手動實現(xiàn)遷移的方法。如果做輕量級的遷移,將最后一個mapping model設(shè)為nil,那么使用本方法和系統(tǒng)實現(xiàn)沒有差別。

    func migrateStoreAt(URL storeURL:NSURL, 
        fromModel from:NSManagedObjectModel, 
        toModel to:NSManagedObjectModel, 
        mappingModel:NSMappingModel? = nil) {
        //......
    }
  8. 最后我們來實現(xiàn)第6步提到的performMigration方法,現(xiàn)在最新的版本是v4,開始之前先做個判斷,當(dāng)前model的最新版本為v4,才執(zhí)行這個performMigration方法下面的內(nèi)容:

    if !currentModel.isVersion4() {
        fatalError("Can only handle migrations to version 4!")
    }

    這樣就變成了從v1 -> v4,v2 -> v4,v3 -> v4的遷移,接下來的方法也很簡單,分別判斷storeModle的版本號,執(zhí)行第7步的migrateStoreAt:方法,并且通過對performMigration方法的遞歸調(diào)用來最終遷移到v4版本。

作者最后還給了兩條建議:

  • 盡量可能采取最簡單的遷移方式,因為遷移很難測試。
  • 每個版本都盡量保存一點數(shù)據(jù)以便將來遷移時可以測試。
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號