原文出處: 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)衡。
本章提供了一個記事本APP,未來數(shù)據(jù)結(jié)構(gòu)要變更,遷移(migration)過程就是:在舊data model的基礎(chǔ)上將數(shù)據(jù)遷移到新的data model中來。
如果僅僅是把Core data當(dāng)做是離線緩存用,那么下次update的時候,丟棄掉就OK了。但是,如果是需要保存用戶的數(shù)據(jù),在下個版本仍然能用,那么就需要遷移數(shù)據(jù)了,具體操作是創(chuàng)建一個新版本的data model,然后提供一個遷移路徑(migration path)。
在創(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可以看做是遷移所需要的地圖。
遷移主要分三步:
這里不用擔(dān)心出錯,Core Data只有遷移成功,才會刪除原始的data store數(shù)據(jù)。
作者根據(jù)日常經(jīng)驗將遷移劃分為四種:
Fully manual migrations
第一種是蘋果的方式,你幾乎不用做什么操作,打開選項遷移就會自動執(zhí)行。第二種需要設(shè)置一個mapping model類似與data model,也是全GUI操作沒什么難度。第三種,就需要你在第二種的基礎(chǔ)上自定義遷移策略(NSEntityMigrationPolicy)供mapping model選擇。最后一種考慮的是如何在多個model版本中跨版本遷移,你要提供相應(yīng)的判定代碼。
所謂輕量級的遷移就是給Note實體增加了一個image的屬性。要做的步驟也很簡單:
作者的做法是在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)系的修改。
- 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).
所以正確的做法就是任何數(shù)據(jù)遷移都應(yīng)先從自動遷移開始,如果搞不定才需要手動遷移。
修改mapping model,分為Attribute Mappings和Relationship Mappings
上圖是實體Note的mapping model,這里的source指的是源數(shù)據(jù)模型(data model)里的Note實體,創(chuàng)建新加實體Attachment的mapping model也很簡單,在Entity Mapping inspector里將source entity改為Note,接著實體Attachment的屬性dateCreated、image就來自于上一版data model里的Note實體。
在Mapping model中可以添加過濾條件,比如設(shè)置NoteToAttachment的Filter Predicate為image != nil,也就是說Attachment的遷移只有在image存在的情況下發(fā)生。
Relationship mapping,這里要注意的一點就是實體Note與Attachment的關(guān)系是在UnCloudNotesDataModel v3這一版本中添加的,所以我們需要的destination relationship其實就是UnCloudNotesDataModel v3中的relationship。于是我們這樣獲得這段關(guān)系
作者這里展示了這個表達式函數(shù):
FUNCTION($manager,
"destinationInstancesForEntityMappingNamed:sourceInstances:",
"NoteToNote", $source)
最后需要更改之前CoreData的options設(shè)置
options:[NSMigratePersistentStoresAutomaticallyOption:true,
NSInferMappingModelAutomaticallyOption:false]
將自動推斷mapping model關(guān)掉,因為我們已經(jīng)自定義了mapping model。
添加UnCloudNotesMappingModel_v3_to_v4,和上一節(jié)類似,NoteToNote mapping和AttachmentToAttachment mappingXcode已經(jīng)為我們設(shè)置OK了,我們只需關(guān)注AttachmentToImageAttachment,修改他的$source為Attachment
除了從父類Attachment繼承而來的屬性,新添加的三個屬性都沒有mapping,我們用代碼來實現(xiàn)吧。
除了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 Inspector里Custom Policy那一欄填入我們上面創(chuàng)建的這個UnCloudNotes.AttachmentToImageAttachmentMigrationPolicyV3toV4。
如果存在多個版本非線性遷移,也就是可能從V1直接到V3或V4...這又該怎么辦呢,這節(jié)代碼比較多,說下思路,就不全帖出來了。
擴展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.
根據(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)
}
增加store和model是否匹配的判斷方法,這里主要用NSPersistentStoreCoordinator的metadataForPersistentStoreOfType方法返回一個metadata,然后再用model的isConfiguration方法對這個metadata進行判斷,來決定model和persistent store是否匹配。
添加兩個計算屬性,storeURL和storeModel,storeModel遍歷所有的model,通過第4步的判斷方法找出相匹配的storeModel。
修改stack的定義:先判斷,store與model不相容,就先執(zhí)行遷移。
var stack: CoreDataStack {
if !storeIsCompatibleWith(Model: currentModel) {
performMigration()
}
return CoreDataStack(modelName: modelName, storeName: storeName, options: options)
}
自定義一個遷移方法,將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) {
//......
}
最后我們來實現(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版本。
作者最后還給了兩條建議:
更多建議: