前言
Core Data是iOS上一個效率比較高的資料庫框架,(但是Core Data並不是一種資料庫,它底層還是利用Sqlite3來儲存資料的),它可以把資料當成物件來操作,而且開發者並不需要在乎資料在磁碟上面的儲存方式。它會把位於NSManagedObject Context裡面的託管物件NSManagedObject類的例項或者某個NSManagedObject子類的例項,通過NSManagedObjectModel託管物件模型,把託管物件儲存到持久化儲存協調器NSPersistentStoreCoordinator持有的一個或者多個持久化儲存區中NSPersistentStore中。使用Core Data進行查詢的語句都是經過Apple特別優化過的,所以都是效率很高的查詢。
當你進行簡單的設定,比如說設定某個實體的預設值,設定級聯刪除的操作,設定資料的驗證規則,使用資料的請求模板,這些修改Core Data都會自己完成,不用自己進行資料遷移。那那些操作需要我們進行資料遷移呢?凡是會引起NSManagedObjectModel託管物件模型變化的,都最好進行資料遷移,防止使用者升級應用之後就閃退。會引起NSManagedObjectModel託管物件模型變化的有以下幾個操作,新增了一張表,新增了一張表裡面的一個實體,新增一個實體的一個屬性,把一個實體的某個屬性遷移到另外一個實體的某個屬性裡面…………大家應該現在都知道哪些操作需要進行資料遷移了吧。
小技巧:
進入正題之前,我先說3個除錯Core Data裡面除錯可能你會需要的操作。
1.一般開啟app沙盒裡面的會有三種型別的檔案,sqlite,sqlite-shm,sqlite-wal,後面2者是iOS7之後系統會預設開啟一個新的“資料庫日誌記錄模式”(database journaling mode)生成的,sqlite-shm是共享記憶體(Shared Memory)檔案,該檔案裡面會包含一份sqlite-wal檔案的索引,系統會自動生成shm檔案,所以刪除它,下次執行還會生成。sqlite-wal是預寫式日誌(Write-Ahead Log)檔案,這個檔案裡面會包含尚未提交的資料庫事務,所以看見有這個檔案了,就代表資料庫裡面還有還沒有處理完的事務需要提交,所以說如果有sqlite-wal檔案,再去開啟sqlite檔案,很可能最近一次資料庫操作還沒有執行。
所以在除錯的時候,我們需要即時的觀察資料庫的變化,我們就可以先禁用這個日誌記錄模式,只需要在建立持久化儲存區的時候存入一個引數即可。具體程式碼如下
1 2 3 4 5 6 7 8 9 10 |
NSDictionary *options = @{ NSSQLitePragmasOption: @{@"journal_mode": @"DELETE"} }; NSError *error = nil; _store = [_coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:options error:&error]; |
2.Mac上開啟資料庫的方式很多,我推薦3個,一個是Firefox裡面直接有sqlite的外掛,免費的,可以直接安裝,也很方便。當然也有不用Firefox的朋友,就像我是Chrome重度使用者,那就推薦2個免費的小的app,一個是sqlitebrowser,一個是sqlite manager,這2個都比較輕量級,都比較好用。
3.如果你想看看Core Data到底底層是如何優化你的查詢語句的,這裡有一個方法可以看到。
先點選Product ->Scheme ->Edit Scheme
然後再切換到Arguments分頁中,在Arguments Passed On Launch裡面加入 “- com.apple.CoreData.SQLDebug 3”,重新執行app,下面就會顯示Core Data優化過的Sql語句了。
好了,除錯資訊應該都可以完美顯示了,可以開始愉快的進入正文了!
一.Core Data自帶的輕量級的資料遷移
這種遷移可別小看它,在你新建一張表的時候還必須加上它才行,否則會出現如下的錯誤,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134100 "(null)" UserInfo={metadata={** ** NSPersistenceFrameworkVersion = 641;** ** NSStoreModelVersionHashes = {** ** Item = ;** ** Measurement = ;** ** };** ** 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}** |
錯誤原因寫的比較清楚了,reason=The model used to open the store is incompatible with the one used to create the store,這個是因為我新建了一張表,但是我沒有開啟輕量級的遷移Option。這裡會有人會問了,我新建表從來沒有出現這個錯誤啊?那是因為你們用的第三方框架就已經寫好了改Option了。(場外人:這年頭誰還自己從0開始寫Core Data啊,肯定都用第三方框架啊)那這裡我就當講解原理了哈。如果是自己從0開始寫的Core Data的話,這裡是應該會報錯了,解決辦法當然是加上程式碼,利用Core Data的輕量級遷移,來防止這種找不到儲存區的閃退問題
1 2 3 4 5 6 7 8 9 10 11 12 |
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]; |
這裡說一下新增加的2個引數的意義:
NSMigratePersistentStoresAutomaticallyOption = YES,那麼Core Data會試著把之前低版本的出現不相容的持久化儲存區遷移到新的模型中,這裡的例子裡,Core Data就能識別出是新表,就會新建出新表的儲存區來,上面就不會報上面的error了。
NSInferMappingModelAutomaticallyOption = YES,這個引數的意義是Core Data會根據自己認為最合理的方式去嘗試MappingModel,從源模型實體的某個屬性,對映到目標模型實體的某個屬性。
接著我們來看看MagicRecord原始碼是怎麼寫的,所以大家才能執行一些操作不會出現我上面說的閃退的問題
1 2 3 4 5 6 7 8 9 10 11 12 13 |
+ (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輕量級的資料遷移的保護了,所以大家不寫那2個引數,一樣不會報錯。(題外話:MagicRecord預設這裡是開啟了WAL日誌記錄模式了) 此處如果大家登出掉那兩個引數,或者把引數的值設定為NO,再執行一次,新建一張表,就會出現我上面提到的錯誤了。大家可以實踐實踐,畢竟實踐出真知嘛。
只要開啟上面2個引數,Core Data就會執行自己的輕量級遷移了,當然,在實體屬性遷移時候,用該方式不靠譜,之前我覺得它肯定能推斷出來,結果後來還是更新後直接閃退報錯了,可能是因為表結構太複雜,超過了它簡單推斷的能力範圍了,所以我建議,在進行復雜的實體屬性遷移到另一個屬性遷移的時候,不要太相信這種方式,還是最好自己Mapping一次。當然,你要是新建一張表的時候,這2個引數是必須要加上的!!!
二.Core Data手動建立Mapping檔案進行遷移
這種方式比前一種方式要更加精細一些,Mapping檔案會指定哪個實體的某個屬性遷移到哪個實體的某個屬性,這比第一種交給Core Data自己去推斷要靠譜一些,這種方法直接指定對映!
先說一下,如果複雜的遷移,不加入這個Mapping檔案會出現什麼樣的錯誤
1 2 3 4 5 6 7 8 9 10 11 |
**Failed to add store. Error: Error Domain=NSCocoaErrorDomain Code=134140 "(null)" UserInfo={destinationModel=() isEditable 0, entities {** ** Amount = "() name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {n qwe = "(), 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 = "() name Item, managedObjectClassName Item, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {n collected = "(), 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 = "(), 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 = "(), 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 = "(), 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 = "(), 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 = " (entity: Item; predicate: (name CONTAINS "e"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";** **}, sourceModel=() isEditable 1, entities {** ** Amount = "() name Amount, managedObjectClassName NSManagedObject, renamingIdentifier Amount, isAbstract 0, superentity name (null), properties {n abc = "(), 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 = "() name Item, managedObjectClassName NSManagedObject, renamingIdentifier Item, isAbstract 0, superentity name (null), properties {n collected = "(), 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 = "(), 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 = "(), 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 = "(), 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 = "(), 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 = " (entity: Item; predicate: (name CONTAINS "e"); sortDescriptors: ((null)); type: NSManagedObjectResultType; )";** **}, reason=Can't find mapping model for migration}** |
直接看最後一行錯誤的原因Can’t find mapping model for migration,這直接說出了錯誤的原因,那麼接下來我們就建立一個Mapping Model檔案。
在你xcdatamodeld相同的資料夾目錄下,“New File” ->”Core Data”->”Mapping Model”
選擇需要Mapping的源資料庫
再選擇目標資料庫
接著命名一下Mapping Model檔案的名字
這裡說明一下,名字最好能一眼看上去就能區分出是哪個資料庫的版本升級上來的,這裡我寫的就是ModelV4ToV5,這樣一看就知道是V4到V5的升級。
這裡說明一下Mapping檔案的重要性,首先,每個版本的資料庫之間都最好能加上一個Mapping檔案,這樣從低版本的資料庫升級上來,可以保證每個版本都不會出錯,都不會導致使用者升級之後就出現閃退的問題。
比如上圖,每個資料庫之間都會對應一個Mapping檔案,V0ToV1,V1ToV2,V2ToV3,V3ToV4,V4ToV5,每個Mapping都必須要。
試想,如果使用者實在V3的老版本上,由於appstore的更新規則,每次更新都直接更新到最新,那麼使用者更新之後就會直接到V5,如果缺少了中間的V3ToV4,V4ToV5,中的任意一個,那麼V3的使用者都無法升級到V5上來,都會閃退。所以這裡就看出了每個版本之間都要加上Mapping檔案的重要性了。這樣任意低版本的使用者,任何時刻都可以通過Mapping檔案,隨意升級到最新版,而且不會閃退了!
接下來再說說Mapping檔案開啟是些什麼東西。
Mapping檔案開啟對應的就是Source源實體屬性,遷移到Target目標實體屬性的對映,上面是屬性,下面是關係的對映。$source就是代表的源實體
寫到這裡,就可以很清楚的區分一下到目前為止,Core Data輕量級遷移和手動建立Mapping進行遷移,這2種方法的異同點了。我簡單總結一下:
1.Core Data輕量級遷移是適用於新增新表,新增新的實體,新增新的實體屬性,等簡單的,系統能自己推斷出來的遷移方式。
2.手動建立Mapping適用於更加複雜的資料遷移
舉個例子吧,假設我最初有一張很抽象的表,叫Object表,用來儲存東西的一些屬性,裡面假設有name,width,height。突然我有一天有新需求了,需要在Object表裡面新增幾個欄位,比如說colour,weight等,由於這個都是簡單的新增,不涉及到資料的轉移,這時候用輕量級遷移就可以了。
不過突然有一個程式又有新需求了,需要增加2張表,一個是Human表,一個是Animal表,需要把當初抽象定義的Object表更加具體化。這時就需要把Object裡面的人都抽出來,放到新建的Human表裡,動物也都抽出來放到新建的Animal表裡。由於新建的2張表都會有name屬性,如果這個時候進行輕量級的遷移,系統可能推斷不出到底哪些name要到Human表裡,哪裡要Animal表了。再者,還有一些屬性在Human表裡面有,在Animal表裡面沒有。這是時候就必須手動新增一個Mapping Model檔案了,手動指定哪些屬性是源實體的屬性,應該對映到目標實體的哪個屬性上面去。這種更加精細的遷移方式,就只能用手動新增Mapping Model來完成了,畢竟iOS系統不知道你的需求和想法。
三.通過程式碼實現資料遷移
這個通過程式碼進行遷移主要是在資料遷移過程中,如果你還想做一些什麼其他事情,比如說你想清理一下垃圾資料,實時展示資料遷移的進度,等等,那就需要在這裡來實現了。
首先,我們需要檢查一下該儲存區存不存在,再把儲存區裡面的model metadata進行比較,檢查一下是否相容,如果不能相容,那麼就需要我們進行資料遷移了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
- (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; } |
當上面函式返回YES,我們就需要合併了,那接下來就是下面的函式了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
- (BOOL)migrateStore:(NSURL*)sourceStore { NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); BOOL success = NO; NSError *error = nil; // STEP 1 - 收集 Source源實體, Destination目標實體 和 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 - 開始執行 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已經完成 } |
上面的函式中,如果遷移進度有變化,會通過觀察者,observeValueForKeyPath來告訴使用者進度,這裡可以監聽該進度,如果沒有完成,可以來禁止使用者執行某些操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
- (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); }); } } |
當然,這個合併資料遷移的操作肯定是用一個多執行緒非同步的執行,免得造成使用者介面卡頓,再加入下面的方法,我們來非同步執行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
- (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);} }); } }); } |
到這裡,資料遷移都完成了,不過目前還有一個問題就是,我們應該何時去執行該遷移的操作,更新完畢之後?appDelegate一進來?都不好,最好的方法還是在把當前儲存區新增到coordinator之前,我們就執行好資料遷移!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
- (void)loadStore { NSLog(@"Running %@ '%@'", self.class, NSStringFromSelector(_cmd)); if (_store) {return;} // 不要再次載入了,因為已經載入過了 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); } } } |
這樣就完成了資料遷移了,並且還能顯示出遷移進度,在遷移中還可以自定義一些操作,比如說清理垃圾資料,刪除一些不用的表,等等。
結束
好了,到此,Core Data資料遷移的幾種方式我就和大家分享完了,如果文中有不對的地方,歡迎大家提出來,我們一起交流進步!
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式