The Migration Problem
Writing Your First Migration
// Step 1: Update your entity
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Long,
val name: String,
val email: String,
val avatarUrl: String? = null // NEW COLUMN (v2)
)
// Step 2: Bump the database version
@Database(
entities = [User::class],
version = 2, // Was 1
exportSchema = true // Always export for testing
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}
// Step 3: Define the migration
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL(
"ALTER TABLE users ADD COLUMN avatarUrl TEXT"
)
}
}
// Step 4: Register the migration
val db = Room.databaseBuilder(
context,
AppDatabase::class.java,
"app.db"
)
.addMigrations(MIGRATION_1_2)
.build()Complex Migrations: New Tables and Data Transforms
// Complex migration: split full_name into first_name + last_name
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(db: SupportSQLiteDatabase) {
// Create new table with updated schema
db.execSQL("""
CREATE TABLE users_new (
id INTEGER PRIMARY KEY NOT NULL,
first_name TEXT NOT NULL DEFAULT '',
last_name TEXT NOT NULL DEFAULT '',
email TEXT NOT NULL,
avatarUrl TEXT
)
""")
// Copy and transform data
db.execSQL("""
INSERT INTO users_new (
id, first_name, last_name, email, avatarUrl
)
SELECT
id,
CASE
WHEN INSTR(name, ' ') > 0
THEN SUBSTR(name, 1, INSTR(name, ' ') - 1)
ELSE name
END,
CASE
WHEN INSTR(name, ' ') > 0
THEN SUBSTR(name, INSTR(name, ' ') + 1)
ELSE ''
END,
email,
avatarUrl
FROM users
""")
// Replace old table
db.execSQL("DROP TABLE users")
db.execSQL("ALTER TABLE users_new RENAME TO users")
}
}Auto-Migrations in Room 2.4+
@Database(
entities = [User::class, Settings::class],
version = 4,
autoMigrations = [
AutoMigration(from = 2, to = 3),
AutoMigration(
from = 3,
to = 4,
spec = Migration3to4::class
),
],
exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun settingsDao(): SettingsDao
}
// Spec needed when Room can't infer the change
@RenameColumn(
tableName = "users",
fromColumnName = "avatar_url",
toColumnName = "profile_image_url"
)
class Migration3to4 : AutoMigrationSpecTesting Migrations
@RunWith(AndroidJUnit4::class)
class MigrationTest {
@get:Rule
val helper = MigrationTestHelper(
InstrumentationRegistry.getInstrumentation(),
AppDatabase::class.java
)
@Test
fun migrate_1_to_2() {
// Create database at version 1
val db = helper.createDatabase("test-db", 1).apply {
execSQL("""
INSERT INTO users (id, name, email)
VALUES (1, 'Jane Doe', '[email protected]')
""")
close()
}
// Run migration and validate
val migratedDb = helper.runMigrationsAndValidate(
"test-db", 2, true, MIGRATION_1_2
)
// Verify data survived
val cursor = migratedDb.query(
"SELECT avatarUrl FROM users WHERE id = 1"
)
assertTrue(cursor.moveToFirst())
assertNull(cursor.getString(0)) // Default null
cursor.close()
}
@Test
fun migrate_all_versions() {
// Test the full migration chain
helper.createDatabase("test-db", 1).close()
helper.runMigrationsAndValidate(
"test-db",
4, // Latest version
true,
MIGRATION_1_2,
MIGRATION_2_3
// Auto-migrations handled by Room
)
}
}Destructive Fallback: When to Use It
Key Takeaways
- 1Always set exportSchema = true for migration testing.
- 2Use auto-migrations for simple changes; manual migrations for data transforms.
- 3Test every migration with MigrationTestHelper before releasing.
- 4Never use destructive fallback for user-generated data.
- 5Keep all exported schema JSON files in version control.
- 6The create-copy-drop-rename pattern handles complex column changes in SQLite.
Frequently Asked
What if I forget to add a migration?
Room will crash with 'Room can't migrate database' error. Users lose local data. Always test migrations before release. Use autoMigrations for simple schema changes in Room 2.4+.
How do I test migrations?
Create database at old version, run migration, verify schema and data. Use Room's MigrationTestHelper or manually copy database files. Test all migration paths, not just consecutive versions.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.