iOS Core Data 數(shù)據(jù)遷移 指南

前言

Core Data是iOS上一個(gè)效率比較高的數(shù)據(jù)庫(kù)框架,(但是Core Data并不是一種數(shù)據(jù)庫(kù)捌省,它底層還是利用Sqlite3來(lái)存儲(chǔ)數(shù)據(jù)的)苫纤,它可以把數(shù)據(jù)當(dāng)成對(duì)象來(lái)操作,而且開(kāi)發(fā)者并不需要在乎數(shù)據(jù)在磁盤(pán)上面的存儲(chǔ)方式所禀。它會(huì)把位于NSManagedObject Context里面的托管對(duì)象NSManagedObject類(lèi)的實(shí)例或者某個(gè)NSManagedObject子類(lèi)的實(shí)例,通過(guò)NSManagedObjectModel托管對(duì)象模型放钦,把托管對(duì)象保存到持久化存儲(chǔ)協(xié)調(diào)器NSPersistentStoreCoordinator持有的一個(gè)或者多個(gè)持久化存儲(chǔ)區(qū)中NSPersistentStore中色徘。使用Core Data進(jìn)行查詢(xún)的語(yǔ)句都是經(jīng)過(guò)Apple特別優(yōu)化過(guò)的,所以都是效率很高的查詢(xún)操禀。

當(dāng)你進(jìn)行簡(jiǎn)單的設(shè)定褂策,比如說(shuō)設(shè)定某個(gè)實(shí)體的默認(rèn)值,設(shè)定級(jí)聯(lián)刪除的操作颓屑,設(shè)定數(shù)據(jù)的驗(yàn)證規(guī)則斤寂,使用數(shù)據(jù)的請(qǐng)求模板,這些修改Core Data都會(huì)自己完成揪惦,不用自己進(jìn)行數(shù)據(jù)遷移遍搞。那那些操作需要我們進(jìn)行數(shù)據(jù)遷移呢?凡是會(huì)引起NSManagedObjectModel托管對(duì)象模型變化的器腋,都最好進(jìn)行數(shù)據(jù)遷移溪猿,防止用戶(hù)升級(jí)應(yīng)用之后就閃退。會(huì)引起NSManagedObjectModel托管對(duì)象模型變化的有以下幾個(gè)操作纫塌,新增了一張表诊县,新增了一張表里面的一個(gè)實(shí)體,新增一個(gè)實(shí)體的一個(gè)屬性措左,把一個(gè)實(shí)體的某個(gè)屬性遷移到另外一個(gè)實(shí)體的某個(gè)屬性里面…………大家應(yīng)該現(xiàn)在都知道哪些操作需要進(jìn)行數(shù)據(jù)遷移了吧依痊。

小技巧:

進(jìn)入正題之前,我先說(shuō)3個(gè)調(diào)試Core Data里面調(diào)試可能你會(huì)需要的操作怎披。

1.一般打開(kāi)app沙盒里面的會(huì)有三種類(lèi)型的文件胸嘁,sqlite,sqlite-shm,sqlite-wal,后面2者是iOS7之后系統(tǒng)會(huì)默認(rèn)開(kāi)啟一個(gè)新的“數(shù)據(jù)庫(kù)日志記錄模式”(database journaling mode)生成的凉逛,sqlite-shm是共享內(nèi)存(Shared Memory)文件缴渊,該文件里面會(huì)包含一份sqlite-wal文件的索引,系統(tǒng)會(huì)自動(dòng)生成shm文件鱼炒,所以刪除它衔沼,下次運(yùn)行還會(huì)生成蝌借。sqlite-wal是預(yù)寫(xiě)式日志(Write-Ahead Log)文件,這個(gè)文件里面會(huì)包含尚未提交的數(shù)據(jù)庫(kù)事務(wù)指蚁,所以看見(jiàn)有這個(gè)文件了菩佑,就代表數(shù)據(jù)庫(kù)里面還有還沒(méi)有處理完的事務(wù)需要提交,所以說(shuō)如果有sqlite-wal文件凝化,再去打開(kāi)sqlite文件稍坯,很可能最近一次數(shù)據(jù)庫(kù)操作還沒(méi)有執(zhí)行。

所以在調(diào)試的時(shí)候搓劫,我們需要即時(shí)的觀察數(shù)據(jù)庫(kù)的變化瞧哟,我們就可以先禁用這個(gè)日志記錄模式,只需要在建立持久化存儲(chǔ)區(qū)的時(shí)候存入一個(gè)參數(shù)即可枪向。具體代碼如下

    NSDictionary *options =
    @{
          NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
     };
    
    NSError *error = nil;
    _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                        configuration:nil
                                                  URL:[self storeURL]
                                              options:options error:&error];

2.Mac上打開(kāi)數(shù)據(jù)庫(kù)的方式很多勤揩,我推薦3個(gè),一個(gè)是Firefox里面直接有sqlite的插件秘蛔,免費(fèi)的陨亡,可以直接安裝,也很方便深员。當(dāng)然也有不用Firefox的朋友负蠕,就像我是Chrome重度使用者,那就推薦2個(gè)免費(fèi)的小的app倦畅,一個(gè)是sqlitebrowser遮糖,一個(gè)是sqlite manager,這2個(gè)都比較輕量級(jí)叠赐,都比較好用止吁。

3.如果你想看看Core Data到底底層是如何優(yōu)化你的查詢(xún)語(yǔ)句的,這里有一個(gè)方法可以看到燎悍。

先點(diǎn)擊Product ->Scheme ->Edit Scheme


然后再切換到Arguments分頁(yè)中,在Arguments Passed On Launch里面加入 “- com.apple.CoreData.SQLDebug 3”,重新運(yùn)行app敬惦,下面就會(huì)顯示Core Data優(yōu)化過(guò)的Sql語(yǔ)句了。


好了谈山,調(diào)試信息應(yīng)該都可以完美顯示了俄删,可以開(kāi)始愉快的進(jìn)入正文了!

一.Core Data自帶的輕量級(jí)的數(shù)據(jù)遷移

這種遷移可別小看它奏路,在你新建一張表的時(shí)候還必須加上它才行畴椰,否則會(huì)出現(xiàn)如下的錯(cuò)誤,

**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={**
**    NSPersistenceFrameworkVersion = 641;**
**    NSStoreModelVersionHashes =     {**
**        Item = <64288772 72e62096 a8a4914f 83db23c9 13718f81 4417e297 293d0267 79b04acb>;**
**        Measurement = <35717f0e 32cae0d4 57325758 58ed0d11 c16563f2 567dac35 de63d5d8 47849cf7>;**
**    };**
**    NSStoreModelVersionHashesVersion = 3;**
**    NSStoreModelVersionIdentifiers =     (**
**        ""**
**    );**
**    NSStoreType = SQLite;**
**    NSStoreUUID = "9A16746E-0C61-421B-B936-412F0C904FDF";**
**    "_NSAutoVacuumLevel" = 2;**
**}, reason=The model used to open the store is incompatible with the one used to create the store}**

錯(cuò)誤原因?qū)懙谋容^清楚了鸽粉,reason=The model used to open the store is incompatible with the one used to create the store斜脂,這個(gè)是因?yàn)槲倚陆艘粡埍恚俏覜](méi)有打開(kāi)輕量級(jí)的遷移Option触机。這里會(huì)有人會(huì)問(wèn)了帚戳,我新建表從來(lái)沒(méi)有出現(xiàn)這個(gè)錯(cuò)誤扮杌颉?那是因?yàn)槟銈冇玫牡谌娇蚣芫鸵呀?jīng)寫(xiě)好了改Option了片任。(場(chǎng)外人:這年頭誰(shuí)還自己從0開(kāi)始寫(xiě)Core Data啊偏友,肯定都用第三方框架啊)那這里我就當(dāng)講解原理了哈。如果是自己從0開(kāi)始寫(xiě)的Core Data的話(huà)对供,這里是應(yīng)該會(huì)報(bào)錯(cuò)了位他,解決辦法當(dāng)然是加上代碼,利用Core Data的輕量級(jí)遷移产场,來(lái)防止這種找不到存儲(chǔ)區(qū)的閃退問(wèn)題


NSDictionary *options =
    @{
      NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"},
      NSMigratePersistentStoresAutomaticallyOption :@YES,
      NSInferMappingModelAutomaticallyOption:@YES
    };
    
    NSError *error = nil;
    _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                        configuration:nil
                                                  URL:[self storeURL]
                                              options:options error:&error];

這里說(shuō)一下新增加的2個(gè)參數(shù)的意義:
NSMigratePersistentStoresAutomaticallyOption = YES鹅髓,那么Core Data會(huì)試著把之前低版本的出現(xiàn)不兼容的持久化存儲(chǔ)區(qū)遷移到新的模型中,這里的例子里京景,Core Data就能識(shí)別出是新表窿冯,就會(huì)新建出新表的存儲(chǔ)區(qū)來(lái),上面就不會(huì)報(bào)上面的error了醋粟。

NSInferMappingModelAutomaticallyOption = YES,這個(gè)參數(shù)的意義是Core Data會(huì)根據(jù)自己認(rèn)為最合理的方式去嘗試MappingModel靡菇,從源模型實(shí)體的某個(gè)屬性重归,映射到目標(biāo)模型實(shí)體的某個(gè)屬性米愿。

接著我們來(lái)看看MagicRecord源碼是怎么寫(xiě)的,所以大家才能執(zhí)行一些操作不會(huì)出現(xiàn)我上面說(shuō)的閃退的問(wèn)題


+ (NSDictionary *) MR_autoMigrationOptions;
{
    // Adding the journalling mode recommended by apple
    NSMutableDictionary *sqliteOptions = [NSMutableDictionary dictionary];
    [sqliteOptions setObject:@"WAL" forKey:@"journal_mode"];
    
    NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
                             [NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
                             [NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption,
                             sqliteOptions, NSSQLitePragmasOption,
                             nil];
    return options;
}

上面這一段就是MagicRecord源碼里面替大家加的Core Data輕量級(jí)的數(shù)據(jù)遷移的保護(hù)了鼻吮,所以大家不寫(xiě)那2個(gè)參數(shù)育苟,一樣不會(huì)報(bào)錯(cuò)。(題外話(huà):MagicRecord默認(rèn)這里是開(kāi)啟了WAL日志記錄模式了) 此處如果大家注銷(xiāo)掉那兩個(gè)參數(shù)椎木,或者把參數(shù)的值設(shè)置為NO违柏,再運(yùn)行一次佛吓,新建一張表多矮,就會(huì)出現(xiàn)我上面提到的錯(cuò)誤了种柑。大家可以實(shí)踐實(shí)踐件舵,畢竟實(shí)踐出真知嘛铝侵。

只要打開(kāi)上面2個(gè)參數(shù)娇斑,Core Data就會(huì)執(zhí)行自己的輕量級(jí)遷移了燎猛,當(dāng)然龙宏,在實(shí)體屬性遷移時(shí)候玛界,用該方式不靠譜万矾,之前我覺(jué)得它肯定能推斷出來(lái),結(jié)果后來(lái)還是更新后直接閃退報(bào)錯(cuò)了慎框,可能是因?yàn)楸斫Y(jié)構(gòu)太復(fù)雜良狈,超過(guò)了它簡(jiǎn)單推斷的能力范圍了,所以我建議笨枯,在進(jìn)行復(fù)雜的實(shí)體屬性遷移到另一個(gè)屬性遷移的時(shí)候薪丁,不要太相信這種方式遇西,還是最好自己Mapping一次。當(dāng)然窥突,你要是新建一張表的時(shí)候努溃,這2個(gè)參數(shù)是必須要加上的!W栉省梧税!

二.Core Data手動(dòng)創(chuàng)建Mapping文件進(jìn)行遷移

這種方式比前一種方式要更加精細(xì)一些,Mapping文件會(huì)指定哪個(gè)實(shí)體的某個(gè)屬性遷移到哪個(gè)實(shí)體的某個(gè)屬性称近,這比第一種交給Core Data自己去推斷要靠譜一些第队,這種方法直接指定映射!
先說(shuō)一下刨秆,如果復(fù)雜的遷移凳谦,不加入這個(gè)Mapping文件會(huì)出現(xiàn)什么樣的錯(cuò)誤


**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134140 "(null)" UserInfo={destinationModel=(<NSManagedObjectModel: 0x7f82d4935280>) isEditable 0, entities {**
**    Amount = "(<NSEntityDescription: 0x7f82d4931960>) name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {\n    qwe = \"(<NSAttributeDescription: 0x7f82d4930f40>), name qwe, isOptional 1, isTransient 0, entity Amount, renamingIdentifier qwe, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue (null)\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**    Item = "(<NSEntityDescription: 0x7f82d4931a10>) name Item, managedObjectClassName Item, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {\n    collected = \"(<NSAttributeDescription: 0x7f82d4930fd0>), name collected, isOptional 1, isTransient 0, entity Item, renamingIdentifier collected, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 0\";\n    listed = \"(<NSAttributeDescription: 0x7f82d4931060>), name listed, isOptional 1, isTransient 0, entity Item, renamingIdentifier listed, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 1\";\n    name = \"(<NSAttributeDescription: 0x7f82d49310f0>), name name, isOptional 1, isTransient 0, entity Item, renamingIdentifier name, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue New Item\";\n    photoData = \"(<NSAttributeDescription: 0x7f82d4931180>), name photoData, isOptional 1, isTransient 0, entity Item, renamingIdentifier photoData, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 1000 , attributeValueClassName NSData, defaultValue (null)\";\n    quantity = \"(<NSAttributeDescription: 0x7f82d4931210>), name quantity, isOptional 1, isTransient 0, entity Item, renamingIdentifier quantity, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 600 , attributeValueClassName NSNumber, defaultValue 1\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**}, fetch request templates {**
**    Test = "<NSFetchRequest: 0x7f82d49316c0> (entity: Item; predicate: (name CONTAINS \"e\"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";**
**}, sourceModel=(<NSManagedObjectModel: 0x7f82d488e930>) isEditable 1, entities {**
**    Amount = "(<NSEntityDescription: 0x7f82d488f880>) name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {\n    abc = \"(<NSAttributeDescription: 0x7f82d488f9d0>), name abc, isOptional 1, isTransient 0, entity Amount, renamingIdentifier abc, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue (null)\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**    Item = "(<NSEntityDescription: 0x7f82d488fbe0>) name Item, managedObjectClassName NSManagedObject, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {\n    collected = \"(<NSAttributeDescription: 0x7f82d48901c0>), name collected, isOptional 1, isTransient 0, entity Item, renamingIdentifier collected, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 0\";\n    listed = \"(<NSAttributeDescription: 0x7f82d488fd20>), name listed, isOptional 1, isTransient 0, entity Item, renamingIdentifier listed, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 800 , attributeValueClassName NSNumber, defaultValue 1\";\n    name = \"(<NSAttributeDescription: 0x7f82d488fdb0>), name name, isOptional 1, isTransient 0, entity Item, renamingIdentifier name, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 700 , attributeValueClassName NSString, defaultValue New Item\";\n    photoData = \"(<NSAttributeDescription: 0x7f82d488fad0>), name photoData, isOptional 1, isTransient 0, entity Item, renamingIdentifier photoData, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 1000 , attributeValueClassName NSData, defaultValue (null)\";\n    quantity = \"(<NSAttributeDescription: 0x7f82d488fc90>), name quantity, isOptional 1, isTransient 0, entity Item, renamingIdentifier quantity, validation predicates (\\n), warnings (\\n), versionHashModifier (null)\\n userInfo {\\n}, attributeType 600 , attributeValueClassName NSNumber, defaultValue 1\";\n}, subentities {\n}, userInfo {\n}, versionHashModifier (null), uniquenessConstraints (\n)";**
**}, fetch request templates {**
**    Test = "<NSFetchRequest: 0x7f82d488fa60> (entity: Item; predicate: (name CONTAINS \"e\"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";**
**}, reason=Can't find mapping model for migration}**

直接看最后一行錯(cuò)誤的原因Can't find mapping model for migration,這直接說(shuō)出了錯(cuò)誤的原因衡未,那么接下來(lái)我們就創(chuàng)建一個(gè)Mapping Model文件尸执。

在你xcdatamodeld相同的文件夾目錄下,“New File” ->"Core Data"->"Mapping Model"

選擇需要Mapping的源數(shù)據(jù)庫(kù)


再選擇目標(biāo)數(shù)據(jù)庫(kù)


接著命名一下Mapping Model文件的名字


這里說(shuō)明一下缓醋,名字最好能一眼看上去就能區(qū)分出是哪個(gè)數(shù)據(jù)庫(kù)的版本升級(jí)上來(lái)的如失,這里我寫(xiě)的就是ModelV4ToV5,這樣一看就知道是V4到V5的升級(jí)送粱。

這里說(shuō)明一下Mapping文件的重要性褪贵,首先,每個(gè)版本的數(shù)據(jù)庫(kù)之間都最好能加上一個(gè)Mapping文件抗俄,這樣從低版本的數(shù)據(jù)庫(kù)升級(jí)上來(lái)脆丁,可以保證每個(gè)版本都不會(huì)出錯(cuò),都不會(huì)導(dǎo)致用戶(hù)升級(jí)之后就出現(xiàn)閃退的問(wèn)題动雹。

比如上圖槽卫,每個(gè)數(shù)據(jù)庫(kù)之間都會(huì)對(duì)應(yīng)一個(gè)Mapping文件,V0ToV1,V1ToV2,V2ToV3,V3ToV4,V4ToV5,每個(gè)Mapping都必須要胰蝠。

試想歼培,如果用戶(hù)實(shí)在V3的老版本上,由于appstore的更新規(guī)則姊氓,每次更新都直接更新到最新丐怯,那么用戶(hù)更新之后就會(huì)直接到V5,如果缺少了中間的V3ToV4,V4ToV5翔横,中的任意一個(gè)读跷,那么V3的用戶(hù)都無(wú)法升級(jí)到V5上來(lái),都會(huì)閃退禾唁。所以這里就看出了每個(gè)版本之間都要加上Mapping文件的重要性了效览。這樣任意低版本的用戶(hù)无切,任何時(shí)刻都可以通過(guò)Mapping文件,隨意升級(jí)到最新版丐枉,而且不會(huì)閃退了哆键!

接下來(lái)再說(shuō)說(shuō)Mapping文件打開(kāi)是些什么東西。

Mapping文件打開(kāi)對(duì)應(yīng)的就是Source源實(shí)體屬性瘦锹,遷移到Target目標(biāo)實(shí)體屬性的映射籍嘹,上面是屬性,下面是關(guān)系的映射弯院。$source就是代表的源實(shí)體

寫(xiě)到這里辱士,就可以很清楚的區(qū)分一下到目前為止,Core Data輕量級(jí)遷移和手動(dòng)創(chuàng)建Mapping進(jìn)行遷移听绳,這2種方法的異同點(diǎn)了颂碘。我簡(jiǎn)單總結(jié)一下:
1.Core Data輕量級(jí)遷移是適用于添加新表,添加新的實(shí)體椅挣,添加新的實(shí)體屬性头岔,等簡(jiǎn)單的,系統(tǒng)能自己推斷出來(lái)的遷移方式鼠证。
2.手動(dòng)創(chuàng)建Mapping適用于更加復(fù)雜的數(shù)據(jù)遷移

舉個(gè)例子吧峡竣,假設(shè)我最初有一張很抽象的表,叫Object表名惩,用來(lái)存儲(chǔ)東西的一些屬性澎胡,里面假設(shè)有name孕荠,width娩鹉,height。突然我有一天有新需求了稚伍,需要在Object表里面新增幾個(gè)字段弯予,比如說(shuō)colour,weight等个曙,由于這個(gè)都是簡(jiǎn)單的新增锈嫩,不涉及到數(shù)據(jù)的轉(zhuǎn)移,這時(shí)候用輕量級(jí)遷移就可以了垦搬。

不過(guò)突然有一個(gè)程序又有新需求了呼寸,需要增加2張表,一個(gè)是Human表猴贰,一個(gè)是Animal表对雪,需要把當(dāng)初抽象定義的Object表更加具體化。這時(shí)就需要把Object里面的人都抽出來(lái)米绕,放到新建的Human表里瑟捣,動(dòng)物也都抽出來(lái)放到新建的Animal表里馋艺。由于新建的2張表都會(huì)有name屬性,如果這個(gè)時(shí)候進(jìn)行輕量級(jí)的遷移迈套,系統(tǒng)可能推斷不出到底哪些name要到Human表里捐祠,哪里要Animal表了。再者桑李,還有一些屬性在Human表里面有踱蛀,在Animal表里面沒(méi)有。這是時(shí)候就必須手動(dòng)添加一個(gè)Mapping Model文件了贵白,手動(dòng)指定哪些屬性是源實(shí)體的屬性星岗,應(yīng)該映射到目標(biāo)實(shí)體的哪個(gè)屬性上面去。這種更加精細(xì)的遷移方式戒洼,就只能用手動(dòng)添加Mapping Model來(lái)完成了俏橘,畢竟iOS系統(tǒng)不知道你的需求和想法。

三.通過(guò)代碼實(shí)現(xiàn)數(shù)據(jù)遷移

這個(gè)通過(guò)代碼進(jìn)行遷移主要是在數(shù)據(jù)遷移過(guò)程中圈浇,如果你還想做一些什么其他事情寥掐,比如說(shuō)你想清理一下垃圾數(shù)據(jù),實(shí)時(shí)展示數(shù)據(jù)遷移的進(jìn)度磷蜀,等等召耘,那就需要在這里來(lái)實(shí)現(xiàn)了。

首先褐隆,我們需要檢查一下該存儲(chǔ)區(qū)存不存在污它,再把存儲(chǔ)區(qū)里面的model metadata進(jìn)行比較,檢查一下是否兼容庶弃,如果不能兼容衫贬,那么就需要我們進(jìn)行數(shù)據(jù)遷移了。

- (BOOL)isMigrationNecessaryForStore:(NSURL*)storeUrl
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    if (![[NSFileManager defaultManager] fileExistsAtPath:[self storeURL].path])
    {
        NSLog(@"SKIPPED MIGRATION: Source database missing.");
        return NO;
    }
    
    NSError *error = nil;
    NSDictionary *sourceMetadata =
    [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                               URL:storeUrl error:&error];
    NSManagedObjectModel *destinationModel = _coordinator.managedObjectModel;
    
    if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetadata])
    {
        NSLog(@"SKIPPED MIGRATION: Source is already compatible");
        return NO;
    }
    
    return YES;
}

當(dāng)上面函數(shù)返回YES歇攻,我們就需要合并了固惯,那接下來(lái)就是下面的函數(shù)了

- (BOOL)migrateStore:(NSURL*)sourceStore {
    
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    BOOL success = NO;
    NSError *error = nil;
    
    // STEP 1 - 收集 Source源實(shí)體, Destination目標(biāo)實(shí)體 和 Mapping Model文件
    NSDictionary *sourceMetadata = [NSPersistentStoreCoordinator
                                    metadataForPersistentStoreOfType:NSSQLiteStoreType
                                    URL:sourceStore
                                    error:&error];
    
    NSManagedObjectModel *sourceModel =
    [NSManagedObjectModel mergedModelFromBundles:nil
                                forStoreMetadata:sourceMetadata];
    
    NSManagedObjectModel *destinModel = _model;
    
    NSMappingModel *mappingModel =
    [NSMappingModel mappingModelFromBundles:nil
                             forSourceModel:sourceModel
                           destinationModel:destinModel];
    
    // STEP 2 - 開(kāi)始執(zhí)行 migration合并, 前提是 mapping model 不是空,或者存在
    if (mappingModel) {
        NSError *error = nil;
        NSMigrationManager *migrationManager =
        [[NSMigrationManager alloc] initWithSourceModel:sourceModel
                                       destinationModel:destinModel];
        [migrationManager addObserver:self
                           forKeyPath:@"migrationProgress"
                              options:NSKeyValueObservingOptionNew
                              context:NULL];
        
        NSURL *destinStore =
        [[self applicationStoresDirectory]
         URLByAppendingPathComponent:@"Temp.sqlite"];
        
        success =
        [migrationManager migrateStoreFromURL:sourceStore
                                         type:NSSQLiteStoreType options:nil
                             withMappingModel:mappingModel
                             toDestinationURL:destinStore
                              destinationType:NSSQLiteStoreType
                           destinationOptions:nil
                                        error:&error];
        if (success)
        {
            // STEP 3 - 用新的migrated store替換老的store
            if ([self replaceStore:sourceStore withStore:destinStore])
            {
                NSLog(@"SUCCESSFULLY MIGRATED %@ to the Current Model",
                          sourceStore.path);
                [migrationManager removeObserver:self
                                      forKeyPath:@"migrationProgress"];
            }
        }
        else
        {
            NSLog(@"FAILED MIGRATION: %@",error);
        }
    }
    else
    {
        NSLog(@"FAILED MIGRATION: Mapping Model is null");
    }
    
    return YES; // migration已經(jīng)完成
}

上面的函數(shù)中缴守,如果遷移進(jìn)度有變化葬毫,會(huì)通過(guò)觀察者,observeValueForKeyPath來(lái)告訴用戶(hù)進(jìn)度屡穗,這里可以監(jiān)聽(tīng)該進(jìn)度贴捡,如果沒(méi)有完成,可以來(lái)禁止用戶(hù)執(zhí)行某些操作


- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context {
    
    if ([keyPath isEqualToString:@"migrationProgress"]) {
        
        dispatch_async(dispatch_get_main_queue(), ^{
            
            float progress =
            [[change objectForKey:NSKeyValueChangeNewKey] floatValue];
          
            int percentage = progress * 100;
            NSString *string =
            [NSString stringWithFormat:@"Migration Progress: %i%%",
             percentage];
            NSLog(@"%@",string);

        });
    }
}

當(dāng)然村砂,這個(gè)合并數(shù)據(jù)遷移的操作肯定是用一個(gè)多線程異步的執(zhí)行烂斋,免得造成用戶(hù)界面卡頓,再加入下面的方法,我們來(lái)異步執(zhí)行


- (void)performBackgroundManagedMigrationForStore:(NSURL*)storeURL
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    dispatch_async(
                   dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
                       BOOL done = [self migrateStore:storeURL];
                       if(done) {
                           dispatch_async(dispatch_get_main_queue(), ^{
                               NSError *error = nil;
                               _store =
                               [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                                          configuration:nil
                                                                    URL:[self storeURL]
                                                                options:nil
                                                                  error:&error];
                               if (!_store) {
                                   NSLog(@"Failed to add a migrated store. Error: %@",
                                         error);abort();}
                               else {
                                   NSLog(@"Successfully added a migrated store: %@",
                                         _store);}
                           });
                       }
                   });
}

到這里源祈,數(shù)據(jù)遷移都完成了煎源,不過(guò)目前還有一個(gè)問(wèn)題就是,我們應(yīng)該何時(shí)去執(zhí)行該遷移的操作香缺,更新完畢之后手销?appDelegate一進(jìn)來(lái)?都不好图张,最好的方法還是在把當(dāng)前存儲(chǔ)區(qū)添加到coordinator之前锋拖,我們就執(zhí)行好數(shù)據(jù)遷移!

- (void)loadStore
{
    NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd));
    
    if (_store) {return;} // 不要再次加載了祸轮,因?yàn)橐呀?jīng)加載過(guò)了
    
    BOOL useMigrationManager = NO;
    if (useMigrationManager &&
        [self isMigrationNecessaryForStore:[self storeURL]])
    {
        [self performBackgroundManagedMigrationForStore:[self storeURL]];
    }
    else
    {
        NSDictionary *options =
        @{
          NSMigratePersistentStoresAutomaticallyOption:@YES
          ,NSInferMappingModelAutomaticallyOption:@YES
          ,NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"}
          };
        NSError *error = nil;
        _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType
                                            configuration:nil
                                                      URL:[self storeURL]
                                                  options:options
                                                    error:&error];
        if (!_store)
        {
            NSLog(@"Failed to add store. Error: %@", error);abort();
        }
        else
        {
            NSLog(@"Successfully added store: %@", _store);
        }
    }

}

這樣就完成了數(shù)據(jù)遷移了兽埃,并且還能顯示出遷移進(jìn)度,在遷移中還可以自定義一些操作适袜,比如說(shuō)清理垃圾數(shù)據(jù)柄错,刪除一些不用的表,等等苦酱。

結(jié)束

好了售貌,到此,Core Data數(shù)據(jù)遷移的幾種方式我就和大家分享完了疫萤,如果文中有不對(duì)的地方颂跨,歡迎大家提出來(lái),我們一起交流進(jìn)步扯饶!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末恒削,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子尾序,更是在濱河造成了極大的恐慌钓丰,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蹲诀,死亡現(xiàn)場(chǎng)離奇詭異斑粱,居然都是意外死亡弃揽,警方通過(guò)查閱死者的電腦和手機(jī)脯爪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)矿微,“玉大人痕慢,你說(shuō)我怎么就攤上這事∮渴福” “怎么了掖举?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)娜庇。 經(jīng)常有香客問(wèn)我塔次,道長(zhǎng)方篮,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任励负,我火速辦了婚禮藕溅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘继榆。我一直安慰自己巾表,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布略吨。 她就那樣靜靜地躺著集币,像睡著了一般。 火紅的嫁衣襯著肌膚如雪翠忠。 梳的紋絲不亂的頭發(fā)上鞠苟,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音秽之,去河邊找鬼偶妖。 笑死,一個(gè)胖子當(dāng)著我的面吹牛政溃,可吹牛的內(nèi)容都是我干的趾访。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼董虱,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼扼鞋!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起愤诱,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤云头,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后淫半,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體溃槐,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年科吭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了昏滴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡对人,死狀恐怖谣殊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情牺弄,我是刑警寧澤姻几,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響蛇捌,放射性物質(zhì)發(fā)生泄漏抚恒。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一络拌、第九天 我趴在偏房一處隱蔽的房頂上張望柑爸。 院中可真熱鬧,春花似錦盒音、人聲如沸表鳍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)譬圣。三九已至,卻和暖如春雄坪,著一層夾襖步出監(jiān)牢的瞬間厘熟,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工维哈, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留绳姨,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓阔挠,卻偏偏與公主長(zhǎng)得像飘庄,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子购撼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容