基于FMDB的本地数据库版本迭代(iOS)

新到公司,项目里有一个本地数据库,最新接到需求需要维护,经过一番了解之后,甚感头大,本地数据库经过26次本地迭代升级,版本迭代代码就有460行,而且每次迭代本地还要维护.sqlite文件用与新用户的本地数据库初始化。懒惰的我就对这种工序多,代码繁琐的迭代方式产生了怀疑。于是我就去搜索了一下,发现,emmmm,好像博客论坛里的也是这么一回事。

然后我就开始思考数据库迭代所需要做的工作。看看能不能简化这个流程,或者寻找到新的迭代方式。

维护数据库无外乎两点:表结构和旧数据。现行的做法是本地保存一个数据库版本号,每次迭代更新一个版本号,在已有的旧表基础上对表结构进行修改,即新建/删除表,新建/删除列。但是在新安装的App中没有本地保存的版本号,这时候就有两种做法,一是初始版本号为0 ,从最初版本开始递归执行版本迭代方法,执行完所有的版本迭代方法后,版本号升为最新;另一种方法是本地建一个.sqlite文件,保持为最新的表结构,第一次启动时直接将这个文件内的表拷贝到沙盒中并将版本号升为最新。

- (void)upgrade:(NSInteger)oldVersion {
     if (oldVersion >= kCurrentSqliteVersion) {
       return;
     }
     switch (oldVersion) {
        case 0:
          [self upgradeFrom0To1];
          break;
        case 1: //从1版本升级到2版本
          [self upgradeFrom1To2];
          break;
        case 2: //版本拓展:以后若有增加则持续增加
          [self upgradeFrom2To3];
          break;
        default:
          break;
  }
  oldVersion ++;
  // 递归判断是否需要升级:保证老版本从最低升级到当前
  [self upgrade:oldVersion];
}

这种做法最大的问题,是在原表上修改表结构。对于已安装App的更新,只需要执行更新操作;而对于第一次启动,则需要执行全部操作;因此需要记录所有的版本升级操作来兼容任意一个旧版本的升级。

如果不考虑数据,我们更新表结构的方法有两种:

  • 在原有的基础上对表中的列进行增加/删除操作;
  • 删除旧表创建新表。

上面的方案就是第一种更新表结构的方法延伸出来的,那么我尝试从第二种方法考虑。如果我要用新表制作新的表结构,那么对于已有的旧表中的数据,就需要做数据迁移。将旧表中的数据迁移到新表中来,已经废弃的列的数据丢弃,新加的列保持为空或者赋默认值,这样就完成了对这个表的迭代操作。如何对表做数据迁移呢?

result = [db executeUpdate:@"INSERT INTO tableNew (column) SELECT column FROM tableOld"];

这个命令可以将右表中的指定列迁移到左表的指定列中。其中自增主键 “id” 不需要迁移。

基于这个思路,来看看我们都需要做哪些工作。

  • 我们同样需要一个表迭代版本号,来告诉我们需不需要执行版本更新操作;
  • 我们要确定存不存在旧表;
  • 我们需要知道旧表中有哪些列在新表中依然存在。

和现行的方法不同的是,我们的迭代方法是每次都要执行的,方法同样具有创建新表(为被使用过的表名)的职责,这同样也是版本迭代可能存在行为之一。如果我们对所有的表采用同一个版本号管理,那么当我们执行任意一个表的迭代时,所有的表都会执行一次迭代操作,虽然表结构没有发生变化,但是会进行数据迁移。因此我给每一个表都独立了一个版本号,并新建了一张表保存这些版本号,当版本号取不到时,就认为是新建表。当迭代完成后,将新的版本号保存到表中。

我们对表进行迭代,在没有需求要求的情况下,迭代前后的表名应该一致。即新建的表与原表的表名一致,这就要求我们对旧表先改名。所以如果表名修改成功,我们就认为此表存在,反之,如果表名修改失败,用原表名新建表成功,说明这个表本就不存在。

新表中的列是我们当前迭代的内容,旧表中的列由于存在版本差异需要我们去旧表中取。

一切准备就续,就可以着手尝试了。

首先要创建一个版本记录表,表中维护当前数据库的版本号,代码中运行一个将要升级的版本号字典用于比对,在程序运行时,检查是否有表需要迭代升级。

__block BOOL resultVersion;
__block NSMutableDictionary *needUpdate = [NSMutableDictionary dictionary];
[self.databaseQueue inDatabase:^(FMDatabase * _Nonnull db) {
    // 创建版本l记录表
    resultVersion = [db executeUpdate:@"CREATE TABLE IF NOT EXISTS tableVersion (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, version DOUBLE)"];
    if (resultVersion) {
        NSLog(@"创建tableVersion成功");
    } else {
        NSLog(@"创建tableVersion失败");
        return;
    }
    // 代码运行的表版本号.需要维护,版本号默认请从1.0开始
    NSMutableDictionary *versionDict = [NSMutableDictionary         dictionaryWithDictionary:@{@"tableNew" : @1.4}];
    FMResultSet *versions = [db executeQuery:@"SELECT * FROM tableVersion"];
    while ([versions next]) {
        NSString *table = [versions stringForColumn:@"name"];
        double version = [versions doubleForColumn:@"version"];
        double dictVerion = [versionDict[table] doubleValue];
        if (dictVerion > version ) {
            // 已经保存过且版本号需要更新
            [needUpdate addEntriesFromDictionary:@{table : @(dictVerion)}];
        }
        [versionDict removeObjectForKey:table];
    }
    [versions close];
    // 本地没有记录的
    for (NSString *key in versionDict.allKeys) {
        double dictVerion = [versionDict[key] doubleValue];
        [needUpdate addEntriesFromDictionary:@{key : @(dictVerion)}];
        // 因表还未创建,此处版本号设置为0,创建成功后更新为当前版本号
        BOOL resultSaveVersion = [db executeUpdate:@"INSERT INTO tableVersion (name,version) VALUES (?,?)", key, @0];
        if (resultSaveVersion) {
            NSLog(@"保存版本号成功");
        }
    }
}];

对需要升级的表执行升级操作

for (NSString *key in needUpdate.allKeys) {
    double dictVerion = [needUpdate[key] doubleValue];
    [self updateTablesWithTableName:key newVersion:dictVerion];
}

方法中包含创建表的sql语句,表名,列名,和待升级版本号

- (void)updateTablesWithTableName:(NSString *)table newVersion:(double)version{
    // 在此处维护表结构
    if ([@"tableNew" isEqualToString:table]) {
        [self updateTableWithSql:@"CREATE TABLE IF NOT EXISTS tableNew (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, address TEXT)" tableName:@"tableNew" columns:@[@"name", @"address"] newVersion:version];
    }
}

然后开始迭代

  • 先将原表改名,然后创建一个名为原表名的新表,此时可能出现几种情况:
    1.改名失败,新表创建失败:代码有误,请查看语句是否正确。
    2.改名失败,新表创建成功:旧表不存在,此时是第一次创建表。
    3.改名成功,新表创建成功:需要执行数据迁移。

  • 执行数据迁移时,若旧表中没有数据,直接将旧表删除。

  • 若旧表中有数据,先取出数据,然后在数据中取出旧表所有列,比对旧表和新表中相同的列名,拼接语句进行数据迁移。需要注意的是,我这里只是为了方便封装的统一操作,如果你仅仅是修改了某一列的列名,而不希望丢失数据,可以单独去写,比如你想把表中的A列变为B列,就可以这么写

      INSERT INTO tableNew (B) SELECT A FROM tableOld
    
  • 数据迁移结束后,删除旧表并同步版本号

      - (void)updateTableWithSql:(NSString *)sql tableName:(NSString *)name columns:(NSArray *)columns newVersion:(double)version{
          [self.databaseQueue inDatabase:^(FMDatabase * _Nonnull db) {
              // 修改表名
              NSString *sqlAlert= [NSString stringWithFormat:@"ALTER TABLE %@ RENAME TO tableOld",name];
              BOOL resultAlter = [db executeUpdate:sqlAlert];
              if (resultAlter) {
                  NSLog(@"旧表改名成功");
              } else {
                  NSLog(@"改名失败,可能不存在");
              }
              // 创建新的表结构
              BOOL resultCreat = [db executeUpdate:sql];
              if (resultCreat) {
                  NSLog(@"创建新表成功");
              } else {
                  NSLog(@"创建新表失败");
                  // 如果新表创建失败,旧表改名成功,进行回退,并结束迭代
              if (resultAlter) {
                  NSString *sqlAlert= [NSString stringWithFormat:@"ALTER TABLE tableOld RENAME TO %@",name];
                  [db executeUpdate:sqlAlert];
              }
              return;
          }
          if (resultAlter && resultCreat) {
              // 用于接收需要数据迁移的列
              NSMutableArray *oldColumn = [NSMutableArray array];
              // 查询表中的字段
              int olldCount = 0; // 统计旧表数据量,等待验证
              FMResultSet * resultTTemp = [db executeQuery:@"select * from tableOld"];
              while ([resultTTemp next]) {
                  if (olldCount == 0) {
                      for (int i=0; i<resultTTemp.columnCount; i++) {
                          NSString * columnName = [resultTTemp columnNameForIndex:i];
                              if ([columns containsObject:columnName] && ![@"id" isEqualToString:columnName]) {
                                  // 自增主键不需要迁移,迁移中,新表会填充
                                  [oldColumn addObject:columnName];
                              }
                          }
                      }
                      olldCount ++;
                  }
                  [resultTTemp close];
                  if (oldColumn.count > 0) {
                      // 合并
                      NSString *columnStr = [oldColumn componentsJoinedByString:@","];
                      NSString *sqlStr = [NSString stringWithFormat:@"INSERT INTO %@ (%@) SELECT %@ FROM tableOld", name, columnStr, columnStr];
                      BOOL result = [db executeUpdate:sqlStr];
                      if (result) {
                          NSLog(@"数据迁移成功");
                          // 验证
                          NSString *sqlVerify = [NSString stringWithFormat:@"select * from %@", name];
                          FMResultSet * resultTTemp = [db executeQuery:sqlVerify];
                          int count = 0;
                          while ([resultTTemp next]) {
                              count ++;
                          }
                          if (count == olldCount) {
                              // 数据迁移成功,删除旧表
                              [db executeUpdate:@"DROP TABLE tableOld"];
                              // 同步版本号
                              [db executeUpdate:@"UPDATE tableVersion SET version=? WHERE name=?", @(version), name];
                              return;
                          }
                      }
                      // 如果数据迁移失败,进行回退操作
                      NSString *sqlBackNew = [NSString stringWithFormat:@"DROP TABLE %@", name];
                      [db executeUpdate:sqlBackNew];
                      NSString *sqlBackAlter = [NSString stringWithFormat:@"ALTER TABLE tableOld RENAME TO %@", name];
                      [db executeUpdate:sqlBackAlter];
                  } else {
                      // 旧表没有数据
                      // 数据迁移成功,删除旧表
                      [db executeUpdate:@"DROP TABLE tableOld"];
                      // 同步版本号
                      [db executeUpdate:@"UPDATE tableVersion SET version=? WHERE name=?", @(version), name];
                  }
              } else {
                  if (resultCreat) {
                      // 如果新表创建成功,旧表改名失败,表示为第一次创建,此处同步版本号
                      [db executeUpdate:@"UPDATE tableVersion SET version=? WHERE name=?", @(version), name];
                  }
              }
          }];
      }
    

总结:

  1. 此方案将原方案基于时间的纵向增长改变为基于表数量的横向增长,只需要维护表创建和版本号就可以完成迭代。

2.此方案对比原方案,动作更大,需要对表中所有数据进行迁移,一旦出现未知错误可能造成的影响也更大,虽然在执行失败时做了回退操作,但不能完美解决安全问题,需要在安全性方面做更多的改进。

3.附上原方案的大神版介绍,希望各位大佬多加指正。https://www.cnblogs.com/PeterWolf/p/6211905.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342