Skip to content

Java|Kotlin 数据迁移

qiuwenchen edited this page Mar 7, 2024 · 1 revision

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

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

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

数据迁移能力

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

// 创建源数据库和源表
Database sourceDatabase = new Database(sourcePath);
sourceDatabase.createTable("sourceTable", DBSample.INSTANCE);

// 写入待迁移的数据到源表
Sample oldObject1 = new Sample();
oldObject1.id = 1;
oldObject1.content = "oldContent1";
Sample oldObject2 = new Sample();
oldObject2.id = 2;
oldObject2.content = "oldContent2";
sourceDatabase.insertObjects(Arrays.asList(oldObject1, oldObject2), DBSample.allFields(), "sourceTable");

// 创建迁移数据的目标数据库
Database targetDatabase = new Database(targetPath);
// 数据迁移配置
// targetDatabase中的所有表格都调用这个回调,需要迁移的表格需要配置 sourceTable
// 这个配置需要在所有targetDatabase数据操作前配置
targetDatabase.addMigrationSource(sourcePath, new Database.MigrationFilter() {
    @Override
    public void filterMigrate(Database.MigrationInfo info) {
        if(info.table.equals("targetTable")) {
            info.sourceTable = "sourceTable";
        }
    }
});

// 创建数据迁移的目标表格
// 目标表格的字段要在源表中都存在,而且数据类型一致
targetDatabase.createTable("targetTable", DBSample.INSTANCE);

以上代码都可以用 Kotlin 实现,因为篇幅限制,就不单独演示了,下同。

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

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

// 使用目标表格更新数据
targetTable.updateValue("newContent2", DBSample.content, DBSample.id.eq(2));
List<Sample> objects = targetTable.getAllObjects();
assert objects.size() == 2;
assert objects.get(1).content.equals("newContent2");

// 使用目标表格删除数据
targetTable.deleteObjects(DBSample.id.eq(2));
Value count = targetTable.getValue(Column.all().count());
assert count.getInt() == 1;

// 使用目标表格写入新数据
Sample newObject = new Sample();
newObject.id = 3;
newObject.content = "newContent3";
targetTable.insertObject(newObject);

List<String> contents = targetTable.getOneColumnString(DBSample.content, DBSample.id.order(Order.Asc));
assert contents.size() == 2;
assert contents.get(0).equals("oldContent1");
assert contents.get(1).equals("newContent3");

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

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

assert !targetDatabase.isMigrated();
final String[] migratedTable = {null};
targetDatabase.setNotificationWhenMigrated(new Database.MigrationNotification() {
    @Override
    public void onMigrated(Database database, Database.MigrationInfo info) {
        migratedTable[0] = info.sourceTable;
    }
});

while(!targetDatabase.isMigrated()){
    targetDatabase.stepMigration();
}

assert migratedTable[0].equals("sourceTable");

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

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

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

Clone this wiki locally