新到公司,项目里有一个本地数据库,最新接到需求需要维护,经过一番了解之后,甚感头大,本地数据库经过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]; } } }]; }
总结:
- 此方案将原方案基于时间的纵向增长改变为基于表数量的横向增长,只需要维护表创建和版本号就可以完成迭代。
2.此方案对比原方案,动作更大,需要对表中所有数据进行迁移,一旦出现未知错误可能造成的影响也更大,虽然在执行失败时做了回退操作,但不能完美解决安全问题,需要在安全性方面做更多的改进。
3.附上原方案的大神版介绍,希望各位大佬多加指正。https://www.cnblogs.com/PeterWolf/p/6211905.html