Skip to content

Swift 数据迁移

qiuwenchen edited this page Mar 7, 2024 · 4 revisions

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

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

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

数据迁移能力

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

// 创建源数据库和源表
let sourceDatabase = Database(at: sourcePath)
try sourceDatabase.create(table: "sourceTable", of: Sample.self)

// 写入待迁移的数据到源表
let oldObject1 = Sample()
oldObject1.identifier = 1;
oldObject1.description = "oldContent1"
let oldObject2 = Sample()
oldObject2.identifier = 2;
oldObject2.description = "oldContent2"
try sourceDatabase.insert(oldObject1, oldObject2, intoTable: "sourceTable")

// 创建迁移数据的目标数据库
let targetDatabase = Database(at: targetPath)
// 数据迁移配置
// targetDatabase中的所有表格都调用这个回调,需要迁移的表格需要配置 sourceTable 和 sourceDatabase
// 这个配置需要在所有targetDatabase数据操作前配置
targetDatabase.addMigration(sourcePath: sourcePath)({ info in
    if info.table == "targetTable" {
        info.sourceTable = "sourceTable"
    }
})

// 创建数据迁移的目标表格
// 目标表格使用的ORM类要和源表一致
try targetDatabase.create(table: "targetTable", of: Sample.self)

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

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

// 使用目标表格更新数据
try targetDatabase.update(table: "targetTable",
                          on: Sample.Properties.description,
                          with: "newContent2",
                          where: Sample.Properties.identifier == 2)
let objects: [Sample] = try targetDatabase.getObjects(fromTable: "targetTable")
XCTAssertEqual(objects.count, 2)
XCTAssertEqual(objects[1].description, "newContent2")

// 使用目标表格删除数据
try targetDatabase.delete(fromTable: "targetTable", where: Sample.Properties.identifier == 2)
let count = try targetDatabase.getValue(on: Sample.Properties.any.count(), fromTable: "targetTable")
XCTAssertEqual(count?.int32Value ?? 0, 1)

// 使用目标表格写入新数据
let newObject = Sample()
newObject.identifier = 3
newObject.description = "newContent3"
try targetDatabase.insert(newObject, intoTable: "targetTable")

let descriptions: OneColumnValue = try targetDatabase.getColumn(on: Sample.Properties.description,
                                                                fromTable: "targetTable",
                                                                orderBy: [Sample.Properties.identifier.order(.ascending)]))

XCTAssertEqual(descriptions.count, 2)
XCTAssertEqual(descriptions[0].stringValue, "oldContent1")
XCTAssertEqual(descriptions[1].stringValue, "newContent3")

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

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

XCTAssertEqual(targetDatabase.isMigrated(), false)

var migratedTable: String? = nil
targetDatabase.setNotification { database, info in
    if let sourceTable = info?.sourceTable {
        migratedTable = sourceTable
    }
}

repeat {
    XCTAssertNoThrow(try targetDatabase.stepMigration())
} while !targetDatabase.isMigrated()

XCTAssertEqual(targetDatabase.isMigrated(), true)

XCTAssertEqual(migratedTable ?? "", "sourceTable")

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

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

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

Clone this wiki locally