1. 程式人生 > >IOS 資料庫升級資料遷移解決方案

IOS 資料庫升級資料遷移解決方案

前言

很久以前就遇到過資料庫版本升級的引用場景,當時的做法是簡單的刪除舊的資料庫檔案,重建資料庫和表結構,這種暴力升級的方式會導致舊的資料的丟失,現在看來這並不不是一個優雅的解決方案,現在一個新的專案中又使用到了資料庫,我不得不重新考慮這個問題,我希望用一種比較優雅的方式去解決這個問題,以後我們還會遇到類似的場景,我們都想做的更好不是嗎?
理想的情況是:資料庫升級,表結構、主鍵和約束有變化,新的表結構建立之後會自動的從舊的表檢索資料,相同的欄位進行對映遷移資料,而絕大多數的業務場景下的資料庫版本升級是隻涉及到欄位的增減、修改主鍵約束,所以下面要實現的方案也是從最基本的、最常用的業務場景去做一個實現,至於更加複雜的場景,可以在此基礎上進行擴充套件,達到符合自己的預期的。

選型定型

網上搜索了下,並沒有資料庫升級資料遷移簡單完整的解決方案,找到了一些思路

  1. 清除舊的資料,重建表
    優點:簡單
    缺點:資料丟失
  2. 在已有表的基礎上對錶結構進行修改
    優點:能夠保留資料
    缺點:規則比較繁瑣,要建立一個數據庫的欄位配置檔案,然後讀取配置檔案,執行SQL修改表結構、約束和主鍵等等,涉及到跨多個版本的資料庫升級就變得繁瑣並且麻煩了
  3. 建立臨時表,把舊的資料拷貝到臨時表,然後刪除舊的資料表並且把臨時表設定為資料表。
    優點:能夠保留資料,支援表結構的修改,約束、主鍵的變更,實現起來比較簡單
    缺點:實現的步驟比較多

綜合考慮,第三種方法是一個比較靠譜的方案。

主要步驟

根據這個思路,分析了一下資料庫升級了主要步驟大概如下:

  • 獲取資料庫中舊的表
  • 修改表名,新增字尾“_bak”,把舊的表當做備份表
  • 建立新的表
  • 獲取新建立的表
  • 遍歷舊的表和新表,對比取出需要遷移的表的欄位
  • 資料遷移處理
  • 刪除備份表

使用到的SQL語句分析

這些操作都是和資料庫操作有關係的,所以問題的關鍵是對應步驟的SQL語句了,下面分析下用到的主要的SQL語句:

  • 獲取資料庫中舊的表
SELECT * from sqlite_master WHERE type='table' 

結果如下,可以看到有type | name | tbl_name | rootpage | sql 這些資料庫欄位,我們只要用到name也就是資料庫名稱這個欄位就行了

sqlite> SELECT * from sqlite_master WHERE type='table'
   ...> ;
+-------+---------------+---------------+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| type  | name          | tbl_name      | rootpage | sql                                                                                                                                                                                                                   |
+-------+---------------+---------------+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| table | t_message_bak | t_message_bak | 2        | CREATE TABLE "t_message_bak" (messageID TEXT, messageType INTEGER, messageJsonContent TEXT, retriveTimeString INTEGER, postTimeString INTEGER, readState INTEGER, PRIMARY KEY(messageID))                             |
| table | t_message     | t_message     | 4        | CREATE TABLE t_message (
	messageID TEXT, 
	messageType INTEGER,
	messageJsonContent TEXT, 
	retriveTimeString INTEGER, 
	postTimeString INTEGER, 
	readState INTEGER, 
	addColumn INTEGER,
	PRIMARY KEY(messageID)
) |
+-------+---------------+---------------+----------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2 行於資料集 (0.03 秒)
  • 修改表名,新增字尾“_bak”,把舊的表當做備份表
-- 把t_message表修改為t_message_bak表  
ALTER TABLE t_message RENAME TO t_message_bak
  • 獲取表字段資訊
-- 獲取t_message_bak表的欄位資訊
PRAGMA table_info('t_message_bak')

獲取到的表字段資訊如下,可以看到有| cid | name | type | notnull | dflt_value | pk | 這些資料庫欄位,我們只要用到name也就是欄位名稱這個欄位就行了

sqlite> PRAGMA table_info('t_message_bak');
+------+--------------------+---------+---------+------------+------+
| cid  | name               | type    | notnull | dflt_value | pk   |
+------+--------------------+---------+---------+------------+------+
| 0    | messageID          | TEXT    | 0       | NULL       | 1    |
| 1    | messageType        | INTEGER | 0       | NULL       | 0    |
| 2    | messageJsonContent | TEXT    | 0       | NULL       | 0    |
| 3    | retriveTimeString  | INTEGER | 0       | NULL       | 0    |
| 4    | postTimeString     | INTEGER | 0       | NULL       | 0    |
| 5    | readState          | INTEGER | 0       | NULL       | 0    |
+------+--------------------+---------+---------+------------+------+
6 行於資料集 (0.01 秒)
  • 使用子查詢進行資料遷移處理
INSERT INTO t_message(messageID, messageType, messageJsonContent, retriveTimeString, postTimeString, readState) SELECT messageID, messageType, messageJsonContent, retriveTimeString, postTimeString, readState FROM t_message_bak

t_message_bak表中的messageID, messageType, messageJsonContent, retriveTimeString, postTimeString, readState這些欄位的值複製到t_message表中

####程式碼實現 接下來就到了程式碼的實現步驟了


// 建立新的臨時表,把資料匯入臨時表,然後用臨時表替換原表
- (void)baseDBVersionControl {
    NSString * version_old = ValueOrEmpty(MMUserDefault.dbVersion);
    NSString * version_new = [NSString stringWithFormat:@"%@", DB_Version];
    NSLog(@"dbVersionControl before: %@ after: %@",version_old,version_new);
    
    // 資料庫版本升級
    if (version_old != nil && ![version_new isEqualToString:version_old]) {
        
        // 獲取資料庫中舊的表
        NSArray* existsTables = [self sqliteExistsTables];
        NSMutableArray* tmpExistsTables = [NSMutableArray array];
        
        // 修改表名,新增字尾“_bak”,把舊的表當做備份表
        for (NSString* tablename in existsTables) {
            [tmpExistsTables addObject:[NSString stringWithFormat:@"%@_bak", tablename]];
            [self.databaseQueue inDatabase:^(FMDatabase *db) {
                NSString* sql = [NSString stringWithFormat:@"ALTER TABLE %@ RENAME TO %@_bak", tablename, tablename];
                [db executeUpdate:sql];
            }];
        }
        existsTables = tmpExistsTables;
        
        // 建立新的表
        [self initTables];
        
        // 獲取新建立的表
        NSArray* newAddedTables = [self sqliteNewAddedTables];
        
        // 遍歷舊的表和新表,對比取出需要遷移的表的欄位
        NSDictionary* migrationInfos = [self generateMigrationInfosWithOldTables:existsTables newTables:newAddedTables];
        
        // 資料遷移處理
        [migrationInfos enumerateKeysAndObjectsUsingBlock:^(NSString* newTableName, NSArray* publicColumns, BOOL * _Nonnull stop) {
            NSMutableString* colunmsString = [NSMutableString new];
            for (int i = 0; i<publicColumns.count; i++) {
                [colunmsString appendString:publicColumns[i]];
                if (i != publicColumns.count-1) {
                    [colunmsString appendString:@", "];
                }
            }
            NSMutableString* sql = [NSMutableString new];
            [sql appendString:@"INSERT INTO "];
            [sql appendString:newTableName];
            [sql appendString:@"("];
            [sql appendString:colunmsString];
            [sql appendString:@")"];
            [sql appendString:@" SELECT "];
            [sql appendString:colunmsString];
            [sql appendString:@" FROM "];
            [sql appendFormat:@"%@_bak", newTableName];
            
            [self.databaseQueue inDatabase:^(FMDatabase *db) {
                [db executeUpdate:sql];
            }];
        }];
        
        // 刪除備份表
        [self.databaseQueue inDatabase:^(FMDatabase *db) {
            [db beginTransaction];
            for (NSString* oldTableName in existsTables) {
                NSString* sql = [NSString stringWithFormat:@"DROP TABLE IF EXISTS %@", oldTableName];
                [db executeUpdate:sql];
            }
            [db commit];
        }];
        
        MMUserDefault.dbVersion = version_new;

    } else {
        MMUserDefault.dbVersion = version_new;
    }
}

- (NSDictionary*)generateMigrationInfosWithOldTables:(NSArray*)oldTables newTables:(NSArray*)newTables {
    NSMutableDictionary<NSString*, NSArray* >* migrationInfos = [NSMutableDictionary dictionary];
    for (NSString* newTableName in newTables) {
        NSString* oldTableName = [NSString stringWithFormat:@"%@_bak", newTableName];
        if ([oldTables containsObject:oldTableName]) {
            // 獲取表資料庫欄位資訊
            NSArray* oldTableColumns = [self sqliteTableColumnsWithTableName:oldTableName];
            NSArray* newTableColumns = [self sqliteTableColumnsWithTableName:newTableName];
            NSArray* publicColumns = [self publicColumnsWithOldTableColumns:oldTableColumns newTableColumns:newTableColumns];
            
            if (publicColumns.count > 0) {
                [migrationInfos setObject:publicColumns forKey:newTableName];
            }
        }
    }
    return migrationInfos;
}

- (NSArray*)publicColumnsWithOldTableColumns:(NSArray*)oldTableColumns newTableColumns:(NSArray*)newTableColumns {
    NSMutableArray* publicColumns = [NSMutableArray array];
    for (NSString* oldTableColumn in oldTableColumns) {
        if ([newTableColumns containsObject:oldTableColumn]) {
            [publicColumns addObject:oldTableColumn];
        }
    }
    return publicColumns;
}

- (NSArray*)sqliteTableColumnsWithTableName:(NSString*)tableName {
    __block NSMutableArray<NSString*>* tableColumes = [NSMutableArray array];
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        NSString* sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')", tableName];
        FMResultSet *rs = [db executeQuery:sql];
        while ([rs next]) {
            NSString* columnName = [rs stringForColumn:@"name"];
            [tableColumes addObject:columnName];
        }
    }];
    return tableColumes;
}

- (NSArray*)sqliteExistsTables {
    __block NSMutableArray<NSString*>* existsTables = [NSMutableArray array];
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        NSString* sql = @"SELECT * from sqlite_master WHERE type='table'";
        FMResultSet *rs = [db executeQuery:sql];
        while ([rs next]) {
            NSString* tablename = [rs stringForColumn:@"name"];
            [existsTables addObject:tablename];
        }
    }];
    return existsTables;
}

- (NSArray*)sqliteNewAddedTables {
    __block NSMutableArray<NSString*>* newAddedTables = [NSMutableArray array];
    [self.databaseQueue inDatabase:^(FMDatabase *db) {
        NSString* sql = @"SELECT * from sqlite_master WHERE type='table' AND name NOT LIKE '%_bak'";
        FMResultSet *rs = [db executeQuery:sql];
        while ([rs next]) {
            NSString* tablename = [rs stringForColumn:@"name"];
            [newAddedTables addObject:tablename];
        }
    }];
    return newAddedTables;
}

問題

sqlite 刪除表文件的大小不變的問題