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 一直报文件找不到——嗯,检查路径是最容易忽略的。

小提示: 预填充的 .db 文件必须是用 SQLite 工具生成的,不能是随便改个后缀名的文本文件。我建议你用 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 预填充数据库流程 App 首次启动 数据库已存在? 直接使用现有数据库 执行预填充 createFromAsset() 从 assets 目录复制 createFromFile() 从文件系统复制 数据库创建完成

这张图展示了 Room 预填充的核心决策流程。说白了就是:数据库存在就用旧的,不存在就从 assets 或文件复制一份。嗯,逻辑其实很简单,但细节决定成败。

我的建议: 如果你只是需要一些静态数据(比如国家列表),用 createFromAsset() 就够了。如果你需要动态更新数据(比如每周更新的词库),那就用 createFromFile() 配合网络下载。

好了,预填充数据库的内容就这些。记住:预填充不是银弹,它只解决「首次启动时数据从哪来」的问题。如果你需要增量更新,还是得靠 Migration 或者手动插入。嗯,下次咱们聊聊更高级的数据库迁移策略。


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