很多 Android 应用都会把用户设置、缓存、离线内容或业务草稿放在本地数据库里。功能早期,表结构通常很简单;产品迭代几轮后,就会遇到新增字段、拆表、合表、索引优化、默认值补齐等问题。
如果这些变化没有迁移方案,用户一升级 App 就可能遇到崩溃、数据丢失,或者页面展示出一堆不符合预期的旧数据。Room 的迁移能力就是为了解决这类问题:既保留 SQLite 的可控性,又让版本升级过程更容易测试和管理。
Room 会根据 `@Database(version = x)` 中的版本号管理数据库结构。当应用安装后,设备上会保存某个版本的数据库文件。下一次 App 升级,如果代码里的数据库版本号变大,Room 就需要知道如何把旧结构升级到新结构。
举个简单例子,原来的用户表只有 `id` 和 `name`:
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey val id: Long,
val name: String
)后来业务需要展示头像,于是新增 `avatarUrl` 字段:
@Entity(tableName = "user")
data class UserEntity(
@PrimaryKey val id: Long,
val name: String,
val avatarUrl: String
)这时如果只是修改实体类并提升数据库版本,Room 并不知道旧设备上的 `user` 表应该如何新增列。没有迁移逻辑时,常见结果是启动时报错:
A migration from 1 to 2 was required but not found.这不是 Room 麻烦,而是在提醒你:线上用户的数据不能靠猜。
Room 的迁移通过 `Migration` 定义,核心是写清楚从旧版本到新版本要执行哪些 SQL。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE user ADD COLUMN avatarUrl TEXT NOT NULL DEFAULT ''"
)
}
}然后在创建数据库时注册:
val database = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
)
.addMigrations(MIGRATION_1_2)
.build()这里有一个很容易忽略的点:新增非空字段时必须给默认值。因为旧表里已经有历史数据,数据库需要知道这些旧行的 `avatarUrl` 应该填什么。如果直接新增 `TEXT NOT NULL` 但不写 `DEFAULT`,迁移通常会失败。
真实项目里,迁移经常比新增一列更复杂。常见场景包括:
例如把 `user` 表里的昵称字段从 `name` 改成 `nickname`,更稳的做法是创建新表并拷贝:
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("""
CREATE TABLE user_new (
id INTEGER NOT NULL,
nickname TEXT NOT NULL,
avatarUrl TEXT NOT NULL DEFAULT '',
PRIMARY KEY(id)
)
""".trimIndent())
db.execSQL("""
INSERT INTO user_new (id, nickname, avatarUrl)
SELECT id, name, avatarUrl FROM user
""".trimIndent())
db.execSQL("DROP TABLE user")
db.execSQL("ALTER TABLE user_new RENAME TO user")
}
}这种写法看起来比 `ALTER TABLE` 绕一些,但它的好处是结果明确:新表结构完全由你控制,历史数据如何映射也写得很清楚。
Room 支持 AutoMigration,可以处理一些简单结构变化,例如新增表、新增列等。
@Database(
entities = [UserEntity::class],
version = 2,
autoMigrations = [
AutoMigration(from = 1, to = 2)
]
)
abstract class AppDatabase : RoomDatabase()自动迁移适合规则明确、没有复杂数据变换的场景。但只要涉及字段改名、删除字段、数据清洗、默认值需要业务判断,就不要完全依赖自动迁移。数据库迁移的本质不是“让编译通过”,而是“让用户的旧数据正确变成新数据”。
我的建议是:
Room 提供了 `fallbackToDestructiveMigration()`,含义是找不到迁移路径时直接删库重建。
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.fallbackToDestructiveMigration()
.build()这个方法在 Demo 或纯缓存库里很方便,但在正式业务数据里风险很高。它会让问题看起来“解决了”,代价是用户旧数据被清空。
更稳妥的判断方式是:
很多线上事故不是因为开发不知道迁移,而是为了赶进度先用了破坏性迁移,后来业务把缓存变成了重要状态,却没人回头改。
数据库迁移一定要测旧版本升级到新版本。Room 提供了 `MigrationTestHelper`,可以创建旧版本数据库,再执行迁移校验。
@RunWith(AndroidJUnit4::class)
class AppDatabaseMigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrateFrom1To2() {
helper.createDatabase("test.db", 1).apply {
execSQL("INSERT INTO user (id, name) VALUES (1, 'Lulu')")
close()
}
val db = helper.runMigrationsAndValidate(
"test.db",
2,
true,
MIGRATION_1_2
)
val cursor = db.query("SELECT avatarUrl FROM user WHERE id = 1")
assertThat(cursor.moveToFirst()).isTrue()
assertThat(cursor.getString(0)).isEqualTo("")
cursor.close()
}
}这个测试能验证两件事:
对于多版本应用,还要关注跨版本升级。用户不一定每个版本都安装过,可能直接从旧版本升级到最新版本。因此迁移链路必须完整,例如 `1 -> 2 -> 3 -> 4` 都要注册,Room 才能从旧库一步步走到最新结构。
在实际项目里,我会把数据库迁移当成一类明确的工程资产,而不是随手写在数据库初始化旁边。
比较稳的组织方式是:
object DatabaseMigrations {
val MIGRATION_1_2 = object : Migration(1, 2) { ... }
val MIGRATION_2_3 = object : Migration(2, 3) { ... }
val ALL = arrayOf(
MIGRATION_1_2,
MIGRATION_2_3
)
}数据库创建时统一注册:
Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
.addMigrations(*DatabaseMigrations.ALL)
.build()每次修改实体或 DAO 前,建议先问三个问题:
如果答案不清楚,先不要急着改版本号。数据库迁移一旦发到线上,修复成本通常比普通 UI 问题高得多。
这是最典型的问题。Room 的实体类代表新结构,但用户设备上保存的是旧结构。两者之间必须有迁移路径。
旧数据没有这个字段,数据库无法凭空生成值。除非字段可空,否则要提供 `DEFAULT`。
全新安装只能证明新库能创建,不能证明旧库能升级。迁移测试要从旧版本数据库开始。
已经发布过的迁移不要随便改。线上用户可能已经执行过这段迁移,再改历史逻辑会让不同用户的数据库状态变得不可预测。更好的方式是新增后续迁移修正问题。
缓存库可以谨慎使用,业务库尽量避免。只要数据对用户有价值,就应该保留。
Room 迁移的关键不是记住 API,而是建立一种意识:数据库结构是线上契约,不能只照顾新安装用户,也要照顾已经在使用旧版本的用户。
一个可靠的迁移方案通常包含四件事:清楚的版本变化、明确的 SQL、完整的迁移链路、能跑起来的迁移测试。做到这些,本地数据库升级就不会变成每次发版前的隐患。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。