> 原文出處: 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**

上圖是實體**Note**的**mapping model**,這里的source指的是源數據模型(data model)里的Note實體,創建新加實體**Attachment**的*mapping 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 mapping**和**AttachmentToAttachment 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 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版本。
作者最后還給了兩條建議:
* 盡量可能采取最簡單的遷移方式,因為遷移很難測試。
* 每個版本都盡量保存一點數據以便將來遷移時可以測試。