12、预填充数据库:createFromAsset()、createFromFile()、预填充时机、常见问题
好,咱们今天聊聊预填充数据库。说白了,就是你的 App 第一次安装时,数据库里已经自带了一些数据。比如城市列表、常用词库、或者一些配置信息。你总不能让用户一打开 App 先等网络加载吧?那体验太差了。
我个人习惯把预填充看作「离线初始化」的一部分。Room 官方提供了两个方法:createFromAsset() 和 createFromFile()。嗯,咱们一个一个说。
12.1 createFromAsset():从 assets 目录加载
这是最常用的方式。你把一个已经填充好数据的 .db 文件放到 src/main/assets/ 目录下,然后在 Room 构建时指定它。
举个例子:
@Database(entities = [City::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun cityDao(): CityDao
companion object {
fun getInstance(context: Context): AppDatabase {
return Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"my_app.db"
)
.createFromAsset("databases/prefilled.db")
.build()
}
}
}
注意路径。如果你把文件放在 assets/databases/prefilled.db,那参数就是 "databases/prefilled.db"。如果你直接放在 assets/ 根目录,那就写 "prefilled.db"。我在项目中遇到过有人把文件放错位置,结果 Room 一直报文件找不到——嗯,检查路径是最容易忽略的。
sqlite3 命令行或者 DB Browser for SQLite 来创建。
12.2 createFromFile():从文件系统加载
这个方法适合数据库文件不是打包在 APK 里,而是从网络下载或者从外部存储读取的场景。比如你的 App 第一次启动时,从服务器下载一个最新的数据库文件,然后预填充。
val prefillFile = File(context.filesDir, "downloaded.db")
Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"my_app.db"
)
.createFromFile(prefillFile)
.build()
这里有个坑:createFromFile() 接受的是 File 对象,但文件必须存在且可读。我曾经在测试时忘了检查文件是否存在,结果 Room 直接崩溃了。所以建议你在调用之前加个判断:
if (prefillFile.exists()) {
// 使用 createFromFile
} else {
// 回退到空数据库
}
12.3 预填充的时机
你可能会问:Room 什么时候执行预填充?
答案是:第一次创建数据库时。也就是 build() 之后,第一次访问数据库(比如执行查询)之前。如果数据库已经存在,Room 不会重复填充。
为什么会这样?因为 Room 在构建时会检查数据库文件是否存在。如果不存在,它就会从 assets 或文件复制一份过来。如果存在,那就直接用了。
这里有个容易混淆的点:如果你升级了数据库版本(比如从 version 1 到 version 2),并且使用了 fallbackToDestructiveMigration(),Room 会销毁旧数据库并重新创建。这时候预填充会再次执行。但如果你用了 addMigrations(),预填充不会触发——因为数据库没有被销毁。
12.4 常见问题与避坑指南
好,咱们聊聊我踩过的坑。嗯,都是真金白银换来的经验。
问题1:数据库文件版本不匹配
预填充的 .db 文件必须和你的 Room 实体定义完全一致。表名、列名、类型、索引,一个都不能差。我曾经在预填充文件里少加了一列,结果 Room 启动时报错:Expected: ... Found: ...。解决方案很简单:用 Room 的 @Database(exportSchema = true) 导出 schema JSON,然后用它来生成预填充文件。
问题2:文件太大导致 ANR
如果你的预填充数据库有几十 MB,复制操作可能会卡住主线程。Room 的 createFromAsset() 默认是在调用线程执行的。如果你在主线程调用 build(),那复制操作也在主线程。我建议你使用 Room.databaseBuilder(...).build() 在后台线程执行,或者配合 allowMainThreadQueries()(但我不推荐)。
问题3:预填充文件包含 WAL 或 SHM 文件
有些 SQLite 工具会生成 .db-wal 和 .db-shm 文件。这些文件不能用于预填充。你只需要一个纯粹的 .db 文件。如果你用 DB Browser for SQLite,记得在关闭数据库时选择「写入并关闭」,确保 WAL 被合并。
问题4:多进程访问
如果你的 App 使用了多进程(比如 android:process=":remote"),每个进程都会尝试创建数据库。预填充可能会被触发多次。解决方案是:只在主进程进行预填充,其他进程使用 .enableMultiInstanceInvalidation()。
- 预填充只在数据库首次创建时执行
- 文件必须放在 assets 目录或可读的文件路径
- 预填充文件必须与 Room 实体完全匹配
- 大文件建议在后台线程处理
12.5 知识体系图
下面这张图帮你理清预填充的整体流程:
这张图展示了 Room 预填充的核心决策流程。说白了就是:数据库存在就用旧的,不存在就从 assets 或文件复制一份。嗯,逻辑其实很简单,但细节决定成败。
createFromAsset() 就够了。如果你需要动态更新数据(比如每周更新的词库),那就用 createFromFile() 配合网络下载。
好了,预填充数据库的内容就这些。记住:预填充不是银弹,它只解决「首次启动时数据从哪来」的问题。如果你需要增量更新,还是得靠 Migration 或者手动插入。嗯,下次咱们聊聊更高级的数据库迁移策略。
公众号:蓝海资料掘金营,微信deep3321