Skip to content

Objc 数据迁移

qiuwenchen edited this page Mar 7, 2024 · 4 revisions

APP发展早期的数据库库表设计常有考虑不足的情况,一个容易犯的错误是把很多种不同的数据都存储到一个数据库中。这样会有两个问题:

  • SQLite同个数据库不支持并行写入,这样不同表就无法同时更新。
  • 同个数据库承载的逻辑越多,读写也就越多,数据库也就越容易损坏,而且损坏的损失也越大。

为了解决这些问题,就需要把数据表迁移到不同的数据库。数据迁移过程是比较慢的,数据迁移处于中间状态时,总不能阻塞用户使用这个数据相关的功能。如果要使用在迁移过程中的数据,就需要同时读取新表和旧表的数据,写入数据也要考虑迁移状态的问题。这样就会导致数据读写的代码需要维护两套,而且因为很难找到一个时间点界定所有用户的数据都迁移完成了,所以这两套数据读写代码要一直维护着,而且新数据库逻辑也要考虑迁移状态,也要写成两份,这样就很麻烦了。

数据迁移能力

为了解决数据迁移的中间状态问题,WCDB 就提出了一个新概念。由 WCDB 来解决兼容问题,让开发者可以 以迁移已经完成为假定前提 进行开发。同时因为是框架层代码,天然就是 code once, run everywhere,所以开发也不需要花费时间在迁移的灰度上,也无需考虑数据迁移的中间状态。下面是数据迁移功能的配置示例:

// 创建源数据库和源表
WCTDatabase* sourceDatabase = [[WCTDatabase alloc] initWithPath:sourcePath];
BOOL ret = [sourceDatabase createTable:@"sourceSampleTable" withClass:Sample.class];

// 写入待迁移的数据到源表
Sample* oldObject1 = [[Sample alloc] init];
oldObject1.identifier = 1;
oldObject1.content = @"oldContent1";
Sample* oldObject2 = [[Sample alloc] init];
oldObject2.identifier = 2;
oldObject2.content = @"oldContent2";
ret &= [sourceDatabase insertObjects:@[oldObject1, oldObject2] intoTable:@"sourceSampleTable"];

// 创建迁移数据的目标数据库
WCTDatabase* targetDatabase = [[WCTDatabase alloc] initWithPath:targetPath];
// 数据迁移配置
// targetDatabase中的所有表格都调用这个回调,需要迁移的表格需要配置 sourceTable 和 sourceDatabase
// 这个配置需要在所有targetDatabase数据操作前配置
[targetDatabase addMigration:sourcePath withFilter:^(WCTMigrationUserInfo *info) {
    if([info.table isEqualToString:@"targetSampleTable"]) {
        info.sourceTable = @"sourceSampleTable"; //配置数据迁移的源表格名
    }
}];

// 创建数据迁移的目标表格
// 目标表格使用的ORM类要和源表一致
ret &= [targetDatabase createTable:@"targetSampleTable" withClass:Sample.class];

WCDB 还支持配置迁移源表中的部分数据,实现方法是在 WCTMigrationUserInfofilterCondition属性上配置一个筛选部分数据的表达式,借用这个功能可以将同个表的数据拆分到不同的表。

配置好之后,就可以认为数据迁移已经瞬间完成,可以直接使用目标表格来操作源表的数据,示例代码如下:

// 使用目标表格更新数据
ret &= [targetDatabase updateTable:@"targetSampleTable" setProperty:Sample.content toValue:@"newContent2" where: Sample.identifier == 2];
NSArray<Sample*>* objects = [targetDatabase getObjectsOfClass:Sample.class fromTable:@"targetSampleTable"];
NSLog(@"%@", objects[1].content); // 输出 newContent2

// 使用目标表格删除数据
ret &= [database deleteFromTable:@"targetSampleTable" where:Sample.identifier == 2];
WCTValue* count = [targetDatabase getValueOnResultColumn:Sample.allProperties.count() fromTable:@"targetSampleTable"];
NSLog(@"%@", count); // 输出 1

// 使用目标表格写入新数据
Sample* newObject = [[Sample alloc] init];
newObject.identifier = 3;
newObject.content = @"newContent3";
ret &= [targetDatabase insertObject:newObject intoTable:@"targetSampleTable"];
WCTOneColumn* contents = [targetDatabase getColumnOnResultColumn:Sample.content fromTable:@"targetSampleTable"];
NSLog(@"%@", contents); // 输出 oldContent1,newContent3

虽然配置之后可以把数据看做已经完全迁移到目标表格了,但是实际数据还在源表格,数据迁移不可能一瞬间完成,需要额外的逻辑将数据迁移过来。

可以使用-[WCTDatabase enableAutoMigration:]接口配置自动迁移,WCDB 会每隔两秒使用大概0.01秒迁移一次数据,直到数据迁移完成;也可以使用-[WCTDatabase stepMigration]接口手动迁移,自己控制数据迁移的节奏。可以使用-[WCTDatabase setNotificationWhenMigrated:]接口注册迁移进度的监听,每次迁移完一个表格都会回调。下面是迁移过程的使用示例:

NSLog(@"%d", targetDatabase.isMigrated); // 输出 0

[targetDatabase setNotificationWhenMigrated:^(WCTDatabase *database, WCTMigrationBaseInfo *info) {
    NSLog(@"Table %@ at database %@ is migrated.", info.sourceTable, info.database);
}];

do {
  	[targetDatabase stepMigration];
} while(!targetDatabase.isMigrated)

加密数据库跨数据库迁移的限制

跨数据库迁移时,需要将源数据库 attach 到目标数据库。源数据库如果是加密数据库,就需要在 attach 时将源数据库解密才能 attach 成功。开发者可以在调用-[WCTDatabase addMigration:withSourceCipher:withFilter:]方法时将源数据库的密码一并传入。

一个需要注意的点是, SQLCipher 在 attach 加密数据库时,只支持传加密 Key 这一个参数,其他配置都是用当前进程的默认配置。如果源数据库不是按照默认配置加密的,就无法在 attach 时解密成功了。所以源数据库必须使用当前进程的默认加密配置。开发者可以使用 +[WCTDatabase setDefaultCipherConfiguration:]接口更改当前进程的默认加密配置,不过要处理好全局配置和其他的加密数据库的加密配置的冲突。

Clone this wiki locally