天天看點

《CoreData》系列(二)CoreData資料遷移以及版本更新

CoreData資料遷移以及版本更新

1、概述

為什麼要有資料遷移? 由于CoreData可視化的特殊性,那麼當資料模型發生變化時,相應的sqlite資料庫的表由于不知道model發生了變化,表結構必須相應的做出調整,否則會導緻程式Crash,CoreData的解決方案是通過建立新的sqlite表,然後将舊的資料遷移到新表上得方案來處理。下面分别介紹三種資料遷移的方式,并詳細說明三種遷移方式的應用場景和注意事項。

  • 輕量級的資料遷移方式
  • 預設的遷移方式
  • 使用遷移管理器

1.1、輕量級的資料遷移方式

輕量級的資料遷移,也就是說,并不需要程式員做很多事情就可以完成資料的遷移,是由系統預設進行的資料遷移。 那麼如何進行輕量級的資料遷移呢,當model的表字段發生變化,且應用程式已經釋出過版本時, 此時千萬不能單單修改原model來達到修改model的目的,如果這樣做的話,程式會crash。 正确的做法是,

  1. 建立一個model,并将model命名為model2,并将model2設定為目前model。
  2. 修改NSPersistentStoreCoordinator加載緩存區的配置。具體如下
NSDictionary *option = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
                                 NSInferMappingModelAutomaticallyOption:@(YES),
                                 };
        
        _store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
                                           configuration:nil
                                                     URL:[self storeUrl]
                                                 options:option
                                                   error:&error];
           

tips:使用iCloud開發程式的app,隻能使用這種遷移方式。

1.2、預設的遷移方式

正常情況下,使用輕量級的資料遷移已經足夠了,但是如果由于開發需要,需要将某個Entity下面的某個Attribute遷移到新的Entity下的某個Attribute,那麼輕量級的遷移方式就不能夠滿足需求,這個時候就需要使用預設的遷移方式來進行資料遷移。這裡以一個例子代碼來詳細闡述如何進行預設的遷移

《CoreData》系列(二)CoreData資料遷移以及版本更新
《CoreData》系列(二)CoreData資料遷移以及版本更新

現在要将Model2裡面的Measurement下面的name遷移到Account裡面的下面的xyz屬性下。

  1. 根據model2來建立一個新model,并命名為model3,然後将model3設定為currentmodel。
  2. 添加新的entity,并命名為Account,添加attribute xyz。
  3. 删除model2裡面的Measurement,根據model3建立NSManagerObect的子類Account。
  4. 以model2為soureModel,model3為destinationModel添加一個MappingModel
  5. 按照下圖所示設定映射model即可
  6. 最後記得将NSInferMappingModelAutomaticallyOption設定為Yes(coredata會優先讀取映射model,如果沒有就會自己推斷),至此,預設的遷移方式就算是搞定了。
《CoreData》系列(二)CoreData資料遷移以及版本更新
《CoreData》系列(二)CoreData資料遷移以及版本更新

1.3、遷移管理器

簡單概述下何為遷移管理器,遷移管理器,就是不再使用系統的NSPersistentCoordinator進行資料遷移,而是使用NSMigrationManager進行資料緩存區的遷移。并配合一個資料遷移視圖控制器提供優雅的遷移等待界面。等待界面如下,是不是感覺很醜呢,哈哈。那麼使用遷移管理器的好處又是什麼呢?可以實作更加精細化的資料操作,此外還能向使用者報告遷移進度。有這倆點,還不夠我們去研究下它麼?Let's go!

《CoreData》系列(二)CoreData資料遷移以及版本更新

準備工作

  • 何時啟用遷移管理器,即遷移的時機?
  • 遷移工作如何進行?
  • 遷移完成如何善後?

下面對上面的問題一一來做解答 遷移的時機,遷移工作需要在載入資料庫的時候進行,即上節所講的 loadStore:的時候進行,但是呢?還需要做一些判斷工作。具體代碼如下

- (void)loadStore
{
    if (debug) {
        NSLog(@"Running %@ ,'%@'",[self class], NSStringFromSelector(_cmd));
    }
    
    if (_store) {
        return;
    }
  
    BOOL useMigrateManager = MigrationMode;

    if (useMigrateManager && [self isMigrationNecessaryForStore:[self storeUrl]]) {
        [self performBackgroundManagedMigrationForStore:[self storeUrl]];
    }else{
        NSError *error;
        
        //NSMigratePersistentStoresAutomaticallyOption coreData嘗試将低版本的資料模型向高版本進行遷移
        //NSInferMappingModelAutomaticallyOption    coredata會自動建立遷移模型,會去自動嘗試
        NSDictionary *option = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
                                 NSInferMappingModelAutomaticallyOption:@(YES),
                                 NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};
        
        _store = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
                                           configuration:nil
                                                     URL:[self storeUrl]
                                                 options:option
                                                   error:&error];
        if (!_store) {
            if (debug) {
                NSLog(@"failed load store,error = %@",error);
                abort();
            }
        }
        else/**/{
            NSLog(@"successfully add store : %@",_store);
        }
    }
}
           

其中有開關,用來控制是否使用遷移管理器,以及系統是否需要進行遷移的判斷。系統是否需要遷移的判斷代碼如下

- (BOOL)isMigrationNecessaryForStore:(NSURL *)storeUrl
{
    if (debug) {
        NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
    }
    
    //檔案是否存在,如果不存在認為是使用者裝置上并沒有持久化存儲區,自然不需要遷移
    if (![[NSFileManager defaultManager]fileExistsAtPath:[self storeUrl].path isDirectory:nil]) {
        if (debug) {
            NSLog(@"Skipped Migration, source database missing");
        }
        return NO;
    }
    
    NSError *error                         = nil;
    NSDictionary *sourceMetaData           = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                                                                        URL:storeUrl
                                                                                                      error:&error];

    NSManagedObjectModel *destinationModel = _coordinate.managedObjectModel;
    
    //比較目前對象模型是否與使用者之前安裝的應用持久化存儲區是否相容。如果相容,不需要遷移
    if ([destinationModel isConfiguration:nil compatibleWithStoreMetadata:sourceMetaData]) {
        if (debug) {
            NSLog(@"Skipped Migration, source database is already compatible");
            return NO;
        }
    }
     
    //所有情況都嘗試了,發現還是需要進行資料遷移
    return YES;
}
           

遷移工作如何進行,衆所周知,遷移工作是一項比較耗時間的工作,尤其是在資料庫比較大的情況下,那麼肯定不能放在前台進行,必須放在背景進行,前台展示加載進度,代碼如下

- (void)performBackgroundManagedMigrationForStore:(NSURL *)store
{
    if (debug) {
        NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
    }
    
    UIStoryboard *sb                      = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
    self.migrationVC                      = [sb instantiateViewControllerWithIdentifier:@"migration"];

    UIApplication *app                    = [UIApplication sharedApplication];
    UINavigationController *navigationCtl = (UINavigationController *)[app keyWindow].rootViewController;
    
    [navigationCtl presentViewController:self.migrationVC
                                animated:YES
                              completion:nil];
    
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
       
        BOOL done = [self migrateStore:[self storeUrl]];
        if (done) {
            dispatch_async(dispatch_get_main_queue(), ^{
                NSError *error              = nil;

                NSDictionary *configuration = @{NSMigratePersistentStoresAutomaticallyOption:@(YES),
                                                NSInferMappingModelAutomaticallyOption:@(YES),
                                                NSSQLitePragmasOption:@{@"journal_mode":@"DELETE"}};

                _store                      = [_coordinate addPersistentStoreWithType:NSSQLiteStoreType
                                                   configuration:nil
                                                             URL:[self storeUrl]
                                                         options:configuration
                                                           error:&error];
                if (_store) {
                    if (debug) {
                        NSLog(@"success create store");
                    }
                }else {
                    if (debug) {
                        NSLog(@"failed, error = %@",error);
                    }
                    abort();
                }
                
                [self.migrationVC dismissViewControllerAnimated:YES
                                                     completion:nil];
                
                self.migrationVC = nil;
            });
        }
        
    });
}
           

接下來是,真正的遷移過程

- (BOOL)migrateStore:(NSURL *)store
{
    if (debug) {
        NSLog(@"Running %@ '%@'",[self class],NSStringFromSelector(_cmd));
    }
    
    
    NSDictionary *sourceMeta               = [NSPersistentStoreCoordinator metadataForPersistentStoreOfType:NSSQLiteStoreType
                                                                                                        URL:store
                                                                                                      error:nil];

    NSManagedObjectModel *sourceModel      = [NSManagedObjectModel mergedModelFromBundles:nil
                                                                         forStoreMetadata:sourceMeta];

    NSManagedObjectModel *destinationModel = _model;
    NSMappingModel *mappingModel           = [NSMappingModel mappingModelFromBundles:nil
                                                                      forSourceModel:sourceModel
                                                                    destinationModel:destinationModel];
    if (mappingModel) {
        NSError *error                       = nil;

        NSMigrationManager *migrationManager = [[NSMigrationManager alloc]initWithSourceModel:sourceModel
                                                                             destinationModel:destinationModel];

        [migrationManager addObserver:self
                           forKeyPath:@"migrationProgress"
                              options:NSKeyValueObservingOptionNew
                              context:nil];

        NSURL *destinationStore              = [[self applicationStoreDirectory]URLByAppendingPathComponent:@"temp.sqlite"];
        BOOL success                         = NO;
        success                              = [migrationManager migrateStoreFromURL:store
                                                    type:NSSQLiteStoreType
                                                 options:nil
                                        withMappingModel:mappingModel
                                        toDestinationURL:destinationStore
                                         destinationType:NSSQLiteStoreType
                                      destinationOptions:nil
                                                   error:&error];
        if (success) {
            if (debug) {
                NSLog(@"Migration Successfully!");
            }
            if ([self replaceStore:store withStore:destinationStore]) {
                [migrationManager removeObserver:self forKeyPath:@"migrationProgress" context:NULL];
                [[NSNotificationCenter defaultCenter]postNotificationName:someThingChangedNotification object:nil];
            }
        }else{
            if (debug) {
                NSLog(@"Migration Failed");
            }
        }
    }else{
        if (debug) {
            NSLog(@"Mapping model is NULL");
        }
    }
    return YES;
}
           

最後附上倆個輔助方法,用來觀察遷移過程和替換資料庫的

- (BOOL)replaceStore:(NSURL *)old withStore:(NSURL *)new
{
    BOOL success   = NO;
    NSError *error = nil;
    if ([[NSFileManager defaultManager]removeItemAtURL:old error:&error]) {
        error = nil;
        if ([[NSFileManager defaultManager]moveItemAtURL:new toURL:old error:&error]) {
            success = YES;
        }else {
            if (debug) {
                NSLog(@"failed move new store to old");
            }
        }
    }else{
        if (debug) {
            NSLog(@"failed remove old store");
        }
    }
    return success;
}
           
- (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];
            self.migrationVC.progressView.progress = progress;

            int percenttage                        = progress * 100;
            NSString *string                       = [NSString stringWithFormat:@"Migration Progress %i%%",percenttage];
            self.migrationVC.progressLabel.text    = string;
        });
    }
}
           

至此,三種資料遷移的方式,都已叙述完畢。

2、小結

三種遷移方式,各有各的好處,輕量級的遷移可以配套icloud實作雲端存儲,預設的資料遷移,支援将屬性級别的資料進行任意遷移。遷移管理器,可以管理檔案存儲路徑,并能夠報告遷移進度,我們在開發過程中,應該按照自己的需求合理選擇遷移方式,下一小節結合NSFetchedResultController進行資料的實際應用。