Introduction
Room database migrations require you to define explicit migration paths between schema versions. When the app updates and the database version increases, Room looks for a migration from the old version to the new version. If the migration path is missing, incomplete, or incorrect, the app crashes with an IllegalStateException about migration not being found.
Symptoms
java.lang.IllegalStateException: A migration from 2 to 4 was required but not found- App crashes on first launch after update
Room cannot verify the data integrityerror- Works on fresh install but crashes for existing users
Expected ... but found ...column mismatch error
Example error:
``
java.lang.IllegalStateException: A migration from 3 to 5 was required but not found.
Please provide the necessary Migration path via
RoomDatabase.Builder.addMigration(Migration...) or allow
destructive migrations via RoomDatabase.Builder.fallbackToDestructiveMigration()
Common Causes
- Skipped a version number when adding migrations (e.g., 2 to 4 without 2 to 3)
- Migration SQL does not match the actual schema change
- Added a new
@Entityfield but did not write the migration - Changed a column type without proper SQL migration
autoMigrationsnot configured for incremental version bumps
Step-by-Step Fix
- 1.Define explicit migrations between consecutive versions:
- 2.```kotlin
- 3.val MIGRATION_1_2 = object : Migration(1, 2) {
- 4.override fun migrate(database: SupportSQLiteDatabase) {
- 5.database.execSQL("ALTER TABLE users ADD COLUMN email TEXT")
- 6.}
- 7.}
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS orders " +
"(id TEXT NOT NULL, user_id TEXT NOT NULL, " +
"amount REAL NOT NULL, PRIMARY KEY(id))")
}
}
// Room can chain 1->2->3 automatically val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .addMigrations(MIGRATION_1_2, MIGRATION_2_3) .build() ```
- 1.Use autoMigration for simple schema changes (Room 2.4+):
- 2.```kotlin
- 3.@Database(
- 4.entities = [User::class, Order::class],
- 5.version = 3,
- 6.autoMigrations = [
- 7.AutoMigration(from = 1, to = 2),
- 8.AutoMigration(from = 2, to = 3)
- 9.]
- 10.)
- 11.abstract class AppDatabase : RoomDatabase() {
- 12.abstract fun userDao(): UserDao
- 13.}
- 14.
` - 15.Handle complex autoMigration with a spec:
- 16.```kotlin
- 17.@Database(
- 18.entities = [User::class],
- 19.version = 3,
- 20.autoMigrations = [
- 21.AutoMigration(
- 22.from = 2,
- 23.to = 3,
- 24.spec = AppDatabase.AutoMigration2To3::class
- 25.)
- 26.]
- 27.)
- 28.abstract class AppDatabase : RoomDatabase() {
- 29.@RenameColumn(
- 30.tableName = "User",
- 31.fromColumnName = "user_name",
- 32.toColumnName = "name"
- 33.)
- 34.class AutoMigration2To3 : AutoMigrationSpec
- 35.}
- 36.
` - 37.Use destructive migration as last resort:
- 38.```kotlin
- 39.// WARNING: This deletes all user data
- 40.val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db")
- 41..fallbackToDestructiveMigration()
- 42..build()
// Or only for specific versions val database = Room.databaseBuilder(context, AppDatabase::class.java, "app.db") .fallbackToDestructiveMigrationFrom(1, 2) // Only for versions 1 and 2 .addMigrations(MIGRATION_3_4) .build() ```
- 1.Test migrations in unit tests:
- 2.```kotlin
- 3.@Test
- 4.fun migrate2To3() {
- 5.// Create database at version 2
- 6.val db2 = Room.databaseBuilder(
- 7.ApplicationProvider.getApplicationContext(),
- 8.AppDatabase::class.java,
- 9."migration-test"
- 10.)
- 11..addMigrations(MIGRATION_2_3)
- 12..build()
// Insert test data at version 2 db2.openHelper.writableDatabase.execSQL( "INSERT INTO users (id, name) VALUES ('1', 'Test')" )
// Migrate to version 3 val db3 = Room.databaseBuilder( ApplicationProvider.getApplicationContext(), AppDatabase::class.java, "migration-test" ) .addMigrations(MIGRATION_2_3) .build()
// Verify data survived migration val user = db3.userDao().getUserById("1") assertEquals("Test", user?.name)
db3.close() } ```
Prevention
- Increment version by 1 for each schema change
- Test every migration path from every previous version to the current version
- Use
autoMigrationsfor simple changes (add column, add table) - Keep explicit migrations for complex changes (rename, type change)
- Add CI tests that verify migration from the oldest supported version
- Use
fallbackToDestructiveMigrationonly during early development