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) {
// 处理异常
}
}
云同步思路
本地备份虽然简单,但不够智能。云同步才是王道。但Room本身不提供云同步能力,需要我们自己实现。
常见的云同步思路有三种:
| 方案 | 优点 | 缺点 |
|---|---|---|
| 全量同步 | 实现简单,每次上传整个数据库文件 | 数据量大时耗时长,浪费流量 |
| 增量同步 | 只同步变化的数据,效率高 | 需要记录变更日志,实现复杂 |
| 混合同步 | 首次全量,后续增量 | 需要维护同步状态 |
我个人比较推荐增量同步。虽然实现起来麻烦点,但用户体验好。具体做法是:
- 在每张表里加一个
lastModified字段 - 同步时查询所有
lastModified > 上次同步时间的记录 - 上传到云端,同时下载云端的新数据
- 处理冲突(以服务器为准,或以本地为准)
你想想看,如果用户有几千条笔记,每次全量上传,那得等到什么时候?增量同步几秒钟就搞定了。
版本兼容
数据库版本兼容,是备份恢复里最容易出问题的地方。我踩过这个坑,印象特别深。
假设你的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会把版本号存在这个字段里。
知识体系总览
下面这张图,把本章的核心逻辑串起来了:
说白了,备份恢复这件事,做得好了用户觉得你靠谱,做得不好用户直接给差评。我建议你在项目初期就把这套机制加进去,别等到用户反馈数据丢了再补救——那时候就晚了。
WAL模式下的日志文件。Room默认开启WAL模式,除了.db文件,还有-wal和-shm两个文件。导出时最好把这三个文件一起打包,否则导入后数据可能不完整。
好了,关于数据备份与恢复,核心内容就这些。记住:用户的数据安全,永远是第一位的。