当你在 Room 数据库中修改实体(Entity)时,你需要正确地处理数据库迁移(Migration)。以下是处理方案:
基础说明
1. 理解迁移基础
Room 使用 版本号 来跟踪数据库结构(Schema)的变化。当你修改实体(例如:添加 / 删除列、更改数据类型)时,你必须:
- 增加数据库版本号。
- 提供一个迁移策略,以将数据从旧结构转换到新结构。
2. 使用 Migration 类定义迁移
处理迁移的主要方式是创建一个 Migration 对象,该对象指定了如何将旧结构转换为新结构。这通常通过 SQL 语句 来完成。
步骤:
-
增加数据库版本号 在你的
@Database注解中,增加version的数值(例如,从1增加到2)。kotlin
@Database( entities = [User::class], version = 2, // 从 1 增加到 2 exportSchema = true // 推荐开启,用于跟踪结构变化 ) abstract class AppDatabase : RoomDatabase() { abstract fun userDao(): UserDao } -
创建一个
Migration对象 定义一个从旧版本(例如1)到新版本(例如2)的迁移。使用 SQL 来修改表。kotlin
val MIGRATION_1_TO_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { // 示例:在 "User" 表中添加一个新列 "email" database.execSQL("ALTER TABLE User ADD COLUMN email TEXT") } } -
将迁移添加到数据库构建器在构建数据库实例时,将迁移包含进去。
kotlin
val db = Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "app_database" ) .addMigrations(MIGRATION_1_TO_2) // 添加迁移 .build()
3. 处理复杂迁移
对于更复杂的更改(例如:重命名表 / 列、更改数据类型、拆分表),请使用 两步法:
- 创建一个具有新结构的临时表。
- 将数据从旧表复制到临时表。
- 删除旧表,并将临时表重命名为原来的表名。
示例:重命名列
假设你想将 User 表中的 username 列重命名为 full_name。
kotlin
val MIGRATION_1_TO_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 1. 创建一个具有新结构的临时表
database.execSQL("""
CREATE TABLE IF NOT EXISTS User_new (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
full_name TEXT,
age INTEGER
)
""".trimIndent())
// 2. 将数据从旧表复制到新表
database.execSQL("""
INSERT INTO User_new (id, full_name, age)
SELECT id, username, age FROM User
""".trimIndent())
// 3. 删除旧表并将新表重命名
database.execSQL("DROP TABLE User")
database.execSQL("ALTER TABLE User_new RENAME TO User")
}
}
4. 使用自动迁移(Auto-Migration)(Room 2.4.0+)
对于简单的结构变化(例如:添加一个带有默认值的可空列),如果你启用了 exportSchema 并使用了 @ColumnInfo(defaultValue = "..."),Room 可以 自动生成迁移代码。
步骤:
-
在
@Database中启用exportSchema(推荐)。 -
添加一个带有默认值的新列。
kotlin
@Entity data class User( @PrimaryKey val id: Int, val name: String, @ColumnInfo(defaultValue = "0") val age: Int = 0 // 带有默认值的新列 ) -
增加数据库版本号。Room 会自动生成迁移代码。
kotlin
@Database( entities = [User::class], version = 2, exportSchema = true ) abstract class AppDatabase : RoomDatabase() { ... }注意:自动迁移适用于以下情况:
- 添加带有默认值的可空列。
- 添加
@ColumnInfo(index = true)(创建索引)。 - 更改
@ColumnInfo(defaultValue)(如果列是可空的)。
5. 测试迁移
务必测试迁移以确保数据完整性。使用 Room.inMemoryDatabaseBuilder 进行测试,并验证:
- 数据是否被正确保留。
- 结构变化是否按预期应用。
示例测试:
kotlin
@Test
fun migrationFrom1To2_keepsData() {
// 1. 创建一个版本为 1 的数据库
val dbV1 = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
)
.addMigrations() // 暂时不添加任何迁移
.build()
// 2. 向版本 1 的数据库中插入测试数据
val userDaoV1 = dbV1.userDao()
userDaoV1.insert(User(id = 1, name = "John"))
// 3. 关闭数据库,并使用版本 2 和迁移重新打开它
dbV1.close()
val dbV2 = Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
)
.addMigrations(MIGRATION_1_TO_2)
.build()
// 4. 验证数据是否被保留
val userV2 = dbV2.userDao().getUserById(1)
assertThat(userV2?.email).isNull() // 新列应为 null (如果没有默认值)
dbV2.close()
}
6. 处理边缘情况
- 数据丢失:在删除列之前,务必先迁移数据。
- 约束 :确保带有
NOT NULL约束的新列有默认值,或者在迁移过程中被填充。 - 回滚:Room 不支持自动回滚。请彻底测试迁移以防止数据损坏。
最佳实践
-
始终增加版本号:切勿重复使用或降低数据库版本号。
-
导出结构(Export Schema) :设置
exportSchema = true以生成 JSON 文件来跟踪结构变化(存储在schemas/目录下)。 -
测试迁移:为每一个迁移编写单元测试,以便及早发现问题。
-
使用事务 :将迁移逻辑包装在
database.beginTransaction()和database.setTransactionSuccessful()中,以确保原子性。kotlin
val MIGRATION_1_TO_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.beginTransaction() try { // 迁移逻辑代码放在这里 database.setTransactionSuccessful() } finally { database.endTransaction() } } }
总结
| 方案 | 适用场景 | 实现方式 |
|---|---|---|
Migration 类 |
简单 / 复杂的结构变化 | 定义 SQL 语句来转换数据 |
| 自动迁移 | 简单变化(添加可空列等) | 启用 exportSchema 并使用默认值 |
| 两步法迁移 | 重命名表 / 列、更改数据类型 | 创建临时表、复制数据、重命名 |
通过遵循这些方案,你可以安全地修改 Room 数据库的结构,同时保留用户数据。
实际开发
1、报错截图

2、解决

加上.fallbacktoDestructiveMigration

升级一下版本号,然后再运行,就可以解决报错的问题。