CoreData 從入門到精通(六)模型版本和數(shù)據(jù)遷移

前面幾篇文章中講的所有內(nèi)容掠廓,都是在同一個(gè)模型版本上進(jìn)行操作的握联。但在真實(shí)開發(fā)中亏狰,基本上不會(huì)一直停留在一個(gè)版本上役纹,因?yàn)樾枨笫遣粩嘧兓模f不定什么時(shí)候就需要往模型里添加新的字段暇唾,添加新的模型促脉,甚至是大規(guī)模的重構(gòu);所以數(shù)據(jù)的遷移就顯得尤為重要了策州。
CoreData 中瘸味,數(shù)據(jù)遷移本質(zhì)就是把舊的 SQLite 數(shù)據(jù)庫里的內(nèi)容,復(fù)制到新的 SQLite 數(shù)據(jù)庫里去够挂,讓新的數(shù)據(jù)庫作為默認(rèn)的數(shù)據(jù)存儲(chǔ)旁仿。伴隨著模型版本的變化,新舊兩個(gè)數(shù)據(jù)庫的實(shí)體結(jié)構(gòu)當(dāng)然也是不同的孽糖。這就是說在遷移過程中必須知道新舊兩個(gè)數(shù)據(jù)庫的模型對(duì)應(yīng)關(guān)系枯冈,舊數(shù)據(jù)庫里的數(shù)據(jù)該怎么復(fù)制到新的數(shù)據(jù)庫中。這在 CoreData 中是由 MappingModel 映射模型來決定的办悟。我們所需要做的就是創(chuàng)建 MappingModel 文件尘奏,指定好實(shí)體不同版本間的映射,CoreData 就會(huì)自動(dòng)幫我們完成數(shù)據(jù)遷移病蛉。當(dāng)然如果模型版本的變化比較小炫加,CoreData 是可以自動(dòng)推斷出映射模型的。下面就來詳細(xì)的介紹一下 CoreData 里常用的幾種遷移铡恕。

創(chuàng)建模型版本

在介紹數(shù)據(jù)遷移之前琢感,先來看如何創(chuàng)建新的模型版本,在 Xcode 里模型是通過 .xcdatamodeld 文件來創(chuàng)建的探熔,實(shí)際上這個(gè)文件就是一個(gè)包驹针,里面可以包含不同的模型版本。選中這個(gè)文件诀艰,然后點(diǎn)擊 Editor->Add Model Version... 就可以添加一個(gè)新的模型版本柬甥。


add-model-version-w400

然后會(huì)彈出下面這個(gè)對(duì)話框饮六,默認(rèn)的新的模型會(huì)在原來的基礎(chǔ)上增加一個(gè)數(shù)字,來標(biāo)識(shí)不同的模型版本苛蒲。這個(gè)數(shù)字也是可以更改的卤橄,你可以按照自己的喜好更改成 v2 或者其他的。


version-name-w600

點(diǎn)擊 finish 后就會(huì)看到現(xiàn)在的 LearnCoreData.xcdatamodeld文件可以展開了臂外,里面包含了所有的模型版本文件窟扑,它們是 xcdatamodel 格式的。在右側(cè)的 File Inspector 面板中可以指定當(dāng)前的模型版本,然后程序打包后就會(huì)把選中的模型版本作為當(dāng)前的默認(rèn)版本漏健。


model-version-w300

自動(dòng)推斷映射模型

上面說到對(duì)于一些較小的變化嚎货,CoreData 是可以自動(dòng)推斷映射模型的,從而幫助我們自動(dòng)地完成數(shù)據(jù)遷移蔫浆。針對(duì)下面這些改動(dòng)殖属,CoreData 都可以自動(dòng)的進(jìn)行推斷:

  • 添加一個(gè)屬性
  • 移除一個(gè)屬性
  • 非空的屬性變成可以為空的
  • 可以為空的屬性變成非空屬性并設(shè)置一個(gè)默認(rèn)值
  • 重命名實(shí)體或者屬性(需要設(shè)置 renaming identifier)
  • 添加/刪除 RelationShip
  • 重命名 RelationShip(需要設(shè)置 renaming identifier)
  • 把一個(gè) RelationShip 從 對(duì)一改成對(duì)多,或者把非排序的改成排序的瓦盛。(反過來也是可以的)

上面說到的 renaming identifier 可以在 Model Inspector 進(jìn)行設(shè)置洗显,對(duì)不同版本的對(duì)應(yīng)實(shí)體/屬性設(shè)置相同的 Renaming ID,CoreData 就可以自動(dòng)推斷出對(duì)應(yīng)的映射模型原环。


renaming-identifier-w600

除此之外挠唆,在向 persistentStoreCoordinator 調(diào)用 addPersistentStoreWithType:configuration:URL:options:error: 添加 persistentStore時(shí),需要將 optionsNSMigratePersistentStoresAutomaticallyOptionNSInferMappingModelAutomaticallyOption 兩個(gè) key 設(shè)置為 YES扮念,CoreData 才會(huì)自動(dòng)推斷损搬。

下面我們來看一下,怎么使用自動(dòng)推斷柜与。這是初始版本的 StudentEntity 實(shí)體的結(jié)構(gòu):

StudentEntity-1-w600

下面我們?cè)賱?chuàng)建一個(gè) Model Version,把原來的 StudentEntity嵌灰、ClassEntity弄匕、CourseEntity 的 EntityName 分別修改成 Student、Clazz沽瞭、Course迁匠;Student 里面的字段修改成 name、id 和 age驹溃,另外再添加一個(gè) BOOL 字段 sex城丧,表示性別,默認(rèn)值設(shè)置為 YES豌鹤。


StudentEntity-2-w600

然后為兩個(gè)版本中修改過的實(shí)體名字和屬性字段名字設(shè)置相同的 renaming identifier亡哄。以 Student 的 name 字段為例,舊版的模型中:


studentName-RenamingID-w600

然后新版本的模型中:


new-name-w600

修改好后布疙,暫時(shí)我們先不切換到新版本的模型中蚊惯,先用舊的數(shù)據(jù)庫生成一些測(cè)試數(shù)據(jù)愿卸,然后在沙盒的 Library/Application Support/ 目錄里復(fù)制出里邊的三個(gè)文件,然后用 SQLite 工具打開 .sqlite 的數(shù)據(jù)庫文件查看數(shù)據(jù)庫的的結(jié)構(gòu)截型,和剛存進(jìn)去的內(nèi)容趴荸。

sqlite-w600

這是打開后的 StudentEntity 表,里面隨機(jī)插入了 300 條數(shù)據(jù)宦焦,注意到現(xiàn)在由我們創(chuàng)建的幾個(gè)字段分別是 ZSTUDENTID发钝、ZSTUDENTCLASS、ZSTUDENTNAME波闹。


StudentEntity-v1-w600

現(xiàn)在我們把數(shù)據(jù)庫切換到新版中笼平,然后再運(yùn)行一次程序,重新打開新生成的數(shù)據(jù)庫文件舔痪,就會(huì)看到新版的數(shù)據(jù)庫的結(jié)構(gòu):


StudentEntity-v2-w600

現(xiàn)在 StudentEntity 已經(jīng)變成了 Student寓调,每個(gè)字段也都變成了新的字段名,而且里面也多了我們添加的 sex 字段锄码。這就說明 CoreData 的自動(dòng)推斷成功了夺英。

自定義映射模型

大多數(shù)情況下自動(dòng)推斷就能幫我們完成數(shù)據(jù)的遷移,但當(dāng)數(shù)據(jù)的變化更復(fù)雜時(shí)滋捶,例如如果我們把 Student 里的一個(gè)字段提取出來放到一個(gè)新的字段中去痛悯。就得靠我們手動(dòng)創(chuàng)建 mapping model 了。例如我們現(xiàn)在想把上面 Clazz 表刪除重窟,原來的 Student 中的 clazz 字段用 clazzName 字段來代替载萌。那么這種情況下就需要手動(dòng)來創(chuàng)建 mapping model 了。
在這之前我們先用舊版的數(shù)據(jù)模型插入一些示例的數(shù)據(jù)巡扇,這是插入的 Student 數(shù)據(jù):


Student-data-w600

Clazz 數(shù)據(jù):


Clazz-data-w600

Course 數(shù)據(jù):


Course-data-w600

因?yàn)?Course 和 Student 是多對(duì)多的關(guān)系扭仁,所以還會(huì)有一張關(guān)聯(lián)表:


SCoursesStudents-data-w600

這是插入示例數(shù)據(jù)的代碼:

- (void)insertManyStudents {
    NSSet *science = [self scienceCourses];
    NSSet *art = [self artCourses];
    Clazz *clazz1 = [[Clazz alloc] initWithContext:self.persistentContainer.viewContext];
    clazz1.clazzName = @"文科一班";
    clazz1.classId = 1;
    
    Clazz *clazz2 = [[Clazz alloc] initWithContext:self.persistentContainer.viewContext];
    clazz2.clazzName = @"理科一班";
    clazz2.classId = 2;
    for (NSUInteger i = 0; i < 300; i++) {
        NSString *name = [NSString stringWithFormat:@"student-%u", arc4random_uniform(100000)];
        int16_t age = (int16_t)arc4random_uniform(10) + 10;
        int16_t stuId = (int16_t)arc4random_uniform(INT16_MAX);
        Student *student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.persistentContainer.viewContext];
        student.name = name;
        student.age = age;
        student.id = stuId;
        if (i % 2 == 0) {
            student.clazz = clazz1;
            student.courses = art;
        } else {
            student.clazz = clazz2;
            student.courses = science;
        }
        
    }
    NSError *error;
    
    
    [self.persistentContainer.viewContext save:&error];
}

- (NSSet<Course *> *)scienceCourses {

    Course *physics = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    physics.courseName = @"物理";
    physics.courseId = 1;
    physics.courseChapterCount = 5;

    Course *chemistry = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    chemistry.courseName = @"化學(xué)";
    chemistry.courseId = 2;
    chemistry.courseChapterCount = 9;

    Course *biology = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    biology.courseName = @"生物";
    biology.courseId = 3;
    biology.courseChapterCount = 10;

    NSSet *courses = [NSSet setWithObjects:physics, chemistry, biology, nil];
    return courses;
}

- (NSSet<Course *> *)artCourses {
    Course *chinese = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    chinese.courseName = @"語文";
    chinese.courseId = 4;
    chinese.courseChapterCount = 12;
    
    Course *history = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    history.courseName = @"歷史";
    history.courseId = 5;
    history.courseChapterCount = 19;
    
    Course *geography = [[Course alloc] initWithContext:self.persistentContainer.viewContext];
    geography.courseName = @"地理";
    geography.courseId = 6;
    geography.courseChapterCount = 21;
    
    return [NSSet setWithObjects:chinese, geography, history, nil];
}

然后我們?cè)賮砜匆幌?新創(chuàng)建的 v3 版本的數(shù)據(jù)模型的結(jié)構(gòu):
Student 表


Student-table-w600

Course 表


Course-table-w600

這一次我們不再創(chuàng)建 Clazz 表了,因?yàn)樗?Student 表里的 clazzName 字段代替厅翔。

接下來創(chuàng)建 Mapping Model 文件


Mapping-Model-w600

創(chuàng)建過程中需要選擇 Source data model 和 Destination data model,也就是遷移的舊版和新版數(shù)據(jù)模型版本乖坠,分別選擇 v2 和 v3 版本:


Source-data-model-w600
Target-data-model-w600

最后保存的文件名建議按一定的規(guī)則來命名,后期也方便查找:


Save-mapping-model-w600

然后我們來認(rèn)識(shí)一下 mapping model 的用法刀闷,創(chuàng)建好后熊泵,mapping model 還是會(huì)自動(dòng)推斷出大多數(shù)的字段映射,例如 Student 表中除新添加的 clazzName 字段外甸昏,其他的都可以正確的推斷出來顽分;


StudentToStudent-mapping-w600

當(dāng)然,如果字段名修改過的話施蜜,同樣是不能推斷出來的卒蘸,如 Course 表的字段:


CourseToCourse-mapping-w600

另外每個(gè) Entity Mapping 的名字的命名規(guī)則是以 SourceEntityNameToDestinationEntityName 來命名的,這個(gè)可以在右側(cè)的面板中修改:
Entity-Mapping-name-w600

下面來介紹 mapping model 中會(huì)用到的幾個(gè)對(duì)象:

  • $source - 對(duì)應(yīng)著 NSMigrationSourceObjectKey花墩,可以理解為 Source Model 的一個(gè)實(shí)體對(duì)象
  • $manager - 對(duì)應(yīng)著 NSMigrationManagerKey悬秉,它代表的是 NSMigrationManager 對(duì)象澄步,正是這個(gè)對(duì)象在遷移過程中發(fā)揮著作用,它管理著源對(duì)象和目標(biāo)對(duì)象之間的關(guān)聯(lián)

除了這兩個(gè)和泌,還有幾個(gè)不常用的:

  • $destination -- NSMigrationDestinationObjectKey
  • $entityMapping -- NSMigrationEntityMappingKey
  • $propertyMapping -- NSMigrationPropertyMappingKey
  • $entityPolicy -- NSMigrationEntityPolicyKey

在 mapping model 中可以通過 $ 加對(duì)應(yīng)的名字村缸,直接訪問這幾個(gè)對(duì)象。例如上面圖中 $source.name 就代表源對(duì)象的 name 屬性武氓。同樣的我們就可以把其他未推斷出來的填上:


-w600
-w600

然后再來看 Relationship Mapping 映射:



對(duì)于這種關(guān)聯(lián)到外部表的字段梯皿,相對(duì)于普通字段會(huì)復(fù)雜一些,我們需要通過右側(cè)的面板來進(jìn)行配置县恕,Name 代表 RelationShip 的字段名东羹;Key Path 代表這個(gè)字段對(duì)應(yīng)的源對(duì)象上的字段,對(duì)于 courses 來說就是 $source.courses忠烛;然后是 Mapping Name属提,它代表這個(gè) RelationShip 所關(guān)聯(lián)的外部表的 Entity Mapping,對(duì)于 courses 來說就是 Course 的 Entity Mapping 也就是 CourseToCourse美尸。配置好這些后冤议,Xcode 會(huì)生成一段長(zhǎng)長(zhǎng)的 Value Expression 表達(dá)式:

FUNCTION($manager, "destinationInstancesForEntityMappingNamed:sourceInstances:" , "CourseToCourse", $source.courses)

意思就是調(diào)用 $manager 對(duì)象的 destinationInstancesForEntityMappingNamed:sourceInstances: 方法 CourseToCourse\$source.courses 分別是兩個(gè)傳入?yún)?shù)。 它會(huì)根據(jù) CourseToCourse 的映射規(guī)則生成$source.courses 的目標(biāo)對(duì)象师坎。
同樣的恕酸,我們可以據(jù)此來配置 Course 里的 students 關(guān)系:

-w600

所有字段都配置完后,就可以把 模型版本切換都 v3 然后運(yùn)行程序胯陋。程序在運(yùn)行時(shí)發(fā)現(xiàn)當(dāng)前的 v3 版本數(shù)據(jù)模型和本地存儲(chǔ)的 v2 數(shù)據(jù)庫版本不一致蕊温,就會(huì)自動(dòng)從 bundle 里尋找對(duì)應(yīng) v2 到 v3 的 Mapping Model,依據(jù)自定義的 Mapping Model遏乔,數(shù)據(jù)就會(huì)自動(dòng)遷移完成义矛。
下面來看一下遷移完成的 v3 版本數(shù)據(jù)庫。
Student 表:


Student-table-v3-w600

Course 表:


Course-table-v3-w600

自動(dòng)生成的 Course 和 Student 之間的關(guān)聯(lián)表:


Students-table-v3-w600

可以看到 Student 表中的 clazz 字段已經(jīng)被 clazzName 替換了按灶。同時(shí)其他的數(shù)據(jù)也都沒有丟失症革。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市鸯旁,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌量蕊,老刑警劉巖铺罢,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異残炮,居然都是意外死亡韭赘,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門势就,熙熙樓的掌柜王于貴愁眉苦臉地迎上來泉瞻,“玉大人脉漏,你說我怎么就攤上這事⌒溲溃” “怎么了侧巨?”我有些...
    開封第一講書人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)鞭达。 經(jīng)常有香客問我司忱,道長(zhǎng),這世上最難降的妖魔是什么畴蹭? 我笑而不...
    開封第一講書人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任坦仍,我火速辦了婚禮,結(jié)果婚禮上叨襟,老公的妹妹穿的比我還像新娘繁扎。我一直安慰自己,他們只是感情好糊闽,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開白布梳玫。 她就那樣靜靜地躺著,像睡著了一般墓怀。 火紅的嫁衣襯著肌膚如雪汽纠。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評(píng)論 1 284
  • 那天傀履,我揣著相機(jī)與錄音虱朵,去河邊找鬼。 笑死钓账,一個(gè)胖子當(dāng)著我的面吹牛碴犬,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播梆暮,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼服协,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了啦粹?” 一聲冷哼從身側(cè)響起偿荷,我...
    開封第一講書人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎唠椭,沒想到半個(gè)月后跳纳,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡贪嫂,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年寺庄,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡斗塘,死狀恐怖赢织,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情馍盟,我是刑警寧澤于置,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站朽合,受9級(jí)特大地震影響俱两,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜曹步,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一宪彩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧讲婚,春花似錦尿孔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至物赶,卻和暖如春白指,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背酵紫。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工告嘲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人奖地。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓橄唬,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親参歹。 傳聞我的和親對(duì)象是個(gè)殘疾皇子仰楚,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

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