Skip to content

C++ 数据迁移

qiuwenchen edited this page Mar 10, 2024 · 5 revisions

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

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

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

数据迁移能力

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

// 创建源数据库和源表
WCDB::Database sourceDatabase(sourcePath);
ret = sourceDatabase.createTable<Sample>("sourceSampleTable");

// 写入待迁移的数据到源表
Sample oldObject1;
oldObject1.identifier = 1;
oldObject1.content = "oldContent1";
Sample oldObject2;
oldObject2.identifier = 2;
oldObject2.content = "oldContent2";
ret &= sourceDatabase.insertObjects<Sample>({oldObject1, oldObject2}, "sourceSampleTable");

// 创建迁移数据的目标数据库
WCDB::Database targetDatabase(targetPath);
// 数据迁移配置
// targetDatabase中的所有表格都调用这个回调,需要迁移的表格需要配置 sourceTable 和 sourceDatabase
// 这个配置需要在所有targetDatabase数据操作前配置
targetDatabase.addMigration(sourcePath, WCDB::Data(), [&](WCDB::Database::MigrationInfo &info){
    if(info.table.compare("targetSampleTable") == 0) {
        info.sourceTable = "sourceSampleTable"; //配置数据迁移的源表格名
    }
});

// 创建数据迁移的目标表格
// 目标表格使用的ORM类要和源表一致
ret &= targetDatabase.createTable<Sample>("targetSampleTable");

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

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

// 使用目标表格更新数据
ret &= targetDatabase.updateRow("newContent2", WCDB_FIELD(Sample::content), "targetSampleTable", where: Sample.identifier == 2);
WCDB::OptionalValueArray<Sample> objects = targetDatabase.getAllObjects<Sample>("targetSampleTable");
printf("%s", objects.value()[1].content.c_str()); // 输出 newContent2

// 使用目标表格删除数据
ret &= targetDatabase.deleteObjects("targetSampleTable", WCDB_FIELD(Sample::identifier) == 2);
WCDB::OptionalValue count = targetDatabase.selectValue(Sample::allFields().count(), "targetSampleTable");
printf("%lld", count.value().intValue()); // 输出 1

// 使用目标表格写入新数据
Sample newObject;
newObject.identifier = 3;
newObject.content = "newContent3";
ret &= targetDatabase.insertObjects<Sample>(newObject, "targetSampleTable");
WCDB::OptionalOneColumn contents = targetDatabase.selectOneColumn(WCDB_FIELD(Sample::content), "targetSampleTable");
printf("%s", contents.value()[0].textValue().data()); // 输出 oldContent1
printf("%s", contents.value()[1].textValue().data()); // 输出 newContent3

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

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

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

targetDatabase.setNotificationWhenMigrated([](WCDB::Database &database, const WCDB::Database::MigrationInfo &info){
printf("Table %s at database %s is migrated.", info.sourceTable.data(), info.database.data());
});

// 一直迁移到完
do {
  	targetDatabase.stepMigration();
} while(!targetDatabase.isMigrated())

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

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

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

Clone this wiki locally