28、数据备份与恢复:导出数据库文件、导入数据库、云同步思路、版本兼容

数据备份与恢复,说白了就是给App的数据上份保险。我见过太多开发者,功能做得花里胡哨,结果用户一换手机,数据全没了——那体验,简直灾难。

今天咱们就聊聊Room数据库怎么备份、怎么恢复,以及云同步的思路和版本兼容的那些坑。嗯,这部分内容,我个人觉得是每个Android开发者都应该掌握的硬技能。

为什么需要手动备份?

Room数据库默认存储在App的内部存储空间。用户卸载App、清除数据,或者换手机,数据就没了。你想想看,用户辛辛苦苦记了一年的笔记、记账记录,说没就没,这谁能接受?

所以,我们需要提供一种机制,让用户能主动导出数据库文件,也能在需要时导入回来。

导出数据库文件

导出数据库,本质上就是把Room生成的.db文件复制到外部存储,或者通过Intent分享出去。

我在项目中遇到过一种情况:用户想把数据迁移到新手机,但新手机还没装App。这时候,把数据库文件导出到Downloads目录,用户就能通过文件管理器拷贝走。

核心代码如下:

fun exportDatabase(context: Context, dbName: String, exportPath: String) {
    val dbFile = context.getDatabasePath(dbName)
    if (!dbFile.exists()) {
        // 数据库文件不存在,直接返回
        return
    }
    try {
        val exportFile = File(exportPath, dbName)
        dbFile.inputStream().use { input ->
            exportFile.outputStream().use { output ->
                input.copyTo(output)
            }
        }
        // 导出成功
    } catch (e: IOException) {
        // 处理异常
    }
}

这里有个细节:导出时最好用inputStream().use这种写法,它能自动关闭流。我以前用老式try-catch-finally,代码又长又容易忘关流。

小技巧:导出路径建议用Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS),这样用户容易找到。

导入数据库文件

导入就是导出的逆操作。用户选择一个.db文件,我们把它复制到App的数据库目录下。

但这里有个大坑:导入前必须关闭Room数据库连接。否则文件被占用,复制会失败。

fun importDatabase(context: Context, dbName: String, sourceFile: File) {
    // 先关闭数据库
    Room.databaseBuilder(context, AppDatabase::class.java, dbName)
        .build().close()

    val destFile = context.getDatabasePath(dbName)
    try {
        sourceFile.inputStream().use { input ->
            destFile.outputStream().use { output ->
                input.copyTo(output)
            }
        }
        // 导入成功,需要重启App或重新打开数据库
    } catch (e: IOException) {
        // 处理异常
    }
}
警告:导入时一定要校验文件格式。我曾经遇到过用户导入了一个损坏的.db文件,结果App直接崩溃。建议在导入前检查文件头,或者用SQLite工具验证一下。

云同步思路

本地备份虽然简单,但不够智能。云同步才是王道。但Room本身不提供云同步能力,需要我们自己实现。

常见的云同步思路有三种:

方案 优点 缺点
全量同步 实现简单,每次上传整个数据库文件 数据量大时耗时长,浪费流量
增量同步 只同步变化的数据,效率高 需要记录变更日志,实现复杂
混合同步 首次全量,后续增量 需要维护同步状态

我个人比较推荐增量同步。虽然实现起来麻烦点,但用户体验好。具体做法是:

  1. 在每张表里加一个lastModified字段
  2. 同步时查询所有lastModified > 上次同步时间的记录
  3. 上传到云端,同时下载云端的新数据
  4. 处理冲突(以服务器为准,或以本地为准)

你想想看,如果用户有几千条笔记,每次全量上传,那得等到什么时候?增量同步几秒钟就搞定了。

版本兼容

数据库版本兼容,是备份恢复里最容易出问题的地方。我踩过这个坑,印象特别深。

假设你的App当前数据库版本是3,用户导出了数据库文件。后来App升级到了版本4,表结构变了。这时候用户再导入版本3的数据库文件,直接打开就会崩溃——因为Room发现版本不匹配。

解决方案有两种:

  • 方案一:导入时检查版本号,如果版本低,就执行Migration
  • 方案二:导入后强制走fallbackToDestructiveMigration(),但会丢失数据

我个人建议用方案一。代码示例如下:

fun importWithMigration(context: Context, sourceFile: File) {
    // 先获取源数据库的版本号
    val sourceVersion = getDatabaseVersion(sourceFile)

    if (sourceVersion < CURRENT_VERSION) {
        // 执行迁移逻辑
        runMigration(sourceFile, sourceVersion, CURRENT_VERSION)
    }

    // 然后正常导入
    importDatabase(context, "app_database.db", sourceFile)
}

这里有个关键点:getDatabaseVersion()需要读取SQLite的PRAGMA user_version。Room会把版本号存在这个字段里。

核心要点:备份恢复不是简单的文件复制。你要考虑数据库版本、文件完整性、并发访问等问题。做好这些,用户的数据才算真正安全。

知识体系总览

下面这张图,把本章的核心逻辑串起来了:

数据备份与恢复 导出数据库 • 复制.db文件到外部存储 • 通过Intent分享 • 注意关闭数据库连接 导入数据库 • 文件格式校验 • 版本兼容检查 • 执行Migration 云同步思路 • 全量同步(简单但慢) • 增量同步(高效) • 冲突处理策略 版本兼容 • 检查PRAGMA user_version • 执行Migration升级 • 避免fallbackToDestructive

说白了,备份恢复这件事,做得好了用户觉得你靠谱,做得不好用户直接给差评。我建议你在项目初期就把这套机制加进去,别等到用户反馈数据丢了再补救——那时候就晚了。

避坑指南:我曾经在导出时忘了处理WAL模式下的日志文件。Room默认开启WAL模式,除了.db文件,还有-wal-shm两个文件。导出时最好把这三个文件一起打包,否则导入后数据可能不完整。

好了,关于数据备份与恢复,核心内容就这些。记住:用户的数据安全,永远是第一位的。

公众号:蓝海资料掘金营,微信deep3321