You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java旧项目迁移至KMM:SQLDelight复用已有.db数据库方案咨询

旧Java项目迁移至KMM+SQLDelight的数据库迁移方案

针对新用户直接使用新库、老用户从Assets目录的database.db迁移数据的需求,下面是一套跨平台通用的实现方案:

整体思路

  • 新用户:直接通过SQLDelight初始化全新数据库
  • 老用户:
    1. 将Assets中只读的旧数据库复制到应用可读写的沙盒目录
    2. 读取旧库中的所有数据
    3. 将数据批量写入SQLDelight创建的新数据库
    4. 标记迁移完成,后续直接使用新库

具体实现步骤

1. 配置SQLDelight数据库

先在KMM项目中定义与旧库结构匹配的SQLDelight Schema,确保字段类型完全兼容(比如旧Java SQLite的INTEGER对应SQLDelight的Long,TEXT对应String)。

举个例子,假设旧库有user表:

-- 旧Java项目的user表结构
CREATE TABLE user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER
);

对应的SQLDelight Schema:

-- src/commonMain/sqldelight/com/example/database/AppDatabase.sq
CREATE TABLE user (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL,
    age INTEGER
);

-- 定义基础查询和插入方法
selectAllUsers:
SELECT * FROM user;

insertUser:
INSERT INTO user(name, age)
VALUES (?, ?);

2. 迁移状态标记

为避免重复执行迁移逻辑,需添加一个迁移状态标记。推荐在SQLDelight库中新增专门的状态表,操作更统一:

-- 添加到AppDatabase.sq
CREATE TABLE migration_status (
    migrated INTEGER NOT NULL DEFAULT 0 -- 0=未迁移,1=已迁移
);

3. Android端迁移逻辑

3.1 复制Assets旧数据库到可读写目录

Android Assets目录下的文件是只读的,必须先复制到应用沙盒的数据库目录:

// 可放在DatabaseFactory类中
private fun copyOldDbFromAssets(context: Context) {
    val oldDbFile = context.getDatabasePath("old_database.db")
    if (oldDbFile.exists()) return

    context.assets.open("database.db").use { input ->
        FileOutputStream(oldDbFile).use { output ->
            input.copyTo(output)
        }
    }
}

3.2 读取旧数据并写入新库

用Android原生SQLiteDatabase连接旧库,查询数据后通过SQLDelight事务批量插入新库:

private fun migrateOldData(context: Context, newDb: AppDatabase) {
    val oldDbPath = context.getDatabasePath("old_database.db").absolutePath
    val oldDb = SQLiteDatabase.openDatabase(oldDbPath, null, SQLiteDatabase.OPEN_READONLY)

    val cursor = oldDb.rawQuery("SELECT id, name, age FROM user", null)
    val userQueries = newDb.userQueries

    // 事务确保数据一致性
    userQueries.transaction {
        while (cursor.moveToNext()) {
            val name = cursor.getString(cursor.getColumnIndexOrThrow("name"))
            val age = cursor.getInt(cursor.getColumnIndexOrThrow("age"))
            userQueries.insertUser(name, age.toLong())
        }
    }

    // 标记迁移完成
    newDb.migrationStatusQueries.updateMigrated(1)

    // 清理资源
    cursor.close()
    oldDb.close()
    // 可选:迁移成功后删除旧库节省空间
    File(oldDbPath).delete()
}

3.3 数据库初始化入口

fun createDatabase(context: Context): AppDatabase {
    val driver = AndroidSqliteDriver(AppDatabase.Schema, context, "new_database.db")
    val db = AppDatabase(driver)

    // 检查迁移状态
    val migrated = db.migrationStatusQueries.getMigrated().executeAsOneOrNull() ?: 0
    if (migrated == 0) {
        copyOldDbFromAssets(context)
        migrateOldData(context, db)
    }
    // 新用户直接使用新库,如需初始化默认数据可在此添加

    return db
}

4. iOS端迁移逻辑(如需支持iOS)

如果KMM项目覆盖iOS端,逻辑与Android类似:

4.1 复制Assets旧库到沙盒

private func copyOldDbFromAssets() {
    let fileManager = FileManager.default
    let docsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let oldDbPath = docsDir.appendingPathComponent("old_database.db")
    
    guard !fileManager.fileExists(atPath: oldDbPath.path) else { return }
    guard let sourcePath = Bundle.main.path(forResource: "database", ofType: "db") else { return }
    
    do {
        try fileManager.copyItem(atPath: sourcePath, toPath: oldDbPath.path)
    } catch {
        print("复制旧数据库失败: \(error.localizedDescription)")
    }
}

4.2 迁移数据到新库

用FMDB简化SQLite操作,读取旧库数据后批量插入SQLDelight新库:

private func migrateOldData(to newDb: AppDatabase) {
    let fileManager = FileManager.default
    let docsDir = fileManager.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let oldDbPath = docsDir.appendingPathComponent("old_database.db").path
    
    guard let oldDb = FMDatabase(path: oldDbPath), oldDb.open() else { return }
    defer { oldDb.close() }
    
    guard let resultSet = oldDb.executeQuery("SELECT name, age FROM user", withArgumentsIn: []) else { return }
    
    newDb.userQueries.transaction {
        while resultSet.next() {
            let name = resultSet.string(forColumn: "name")!
            let age = resultSet.longLongInt(forColumn: "age")
            newDb.userQueries.insertUser(name: name, age: age)
        }
    }
    
    // 标记迁移完成
    newDb.migrationStatusQueries.updateMigrated(1)
    
    // 可选:删除旧库文件
    try? fileManager.removeItem(atPath: oldDbPath)
}

4.3 iOS端数据库初始化

func createDatabase() -> AppDatabase {
    let docsDir = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
    let dbPath = docsDir.appendingPathComponent("new_database.db")
    let driver = SqliteDriver(path: dbPath.path)
    let db = AppDatabase(driver: driver)
    
    let migrated = db.migrationStatusQueries.getMigrated().executeAsOneOrNull() ?? 0
    if migrated == 0 {
        copyOldDbFromAssets()
        migrateOldData(to: db)
    }
    
    return db
}

5. 关键注意事项

  • 类型兼容:必须确保旧库字段类型与SQLDelight定义的类型完全匹配,避免数据丢失或插入失败
  • 事务保护:迁移数据时一定要用事务,防止中途崩溃导致数据部分插入
  • 错误处理:添加异常捕获和日志,方便排查迁移失败的问题
  • 测试覆盖:分别测试新用户首次安装、老用户升级两种场景,确保逻辑正常

内容的提问来源于stack exchange,提问作者user238664

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.28 15:18:18