Migrate your Room database

As you add and change features in your app, you need to modify your Room entity classes and underlying database tables to reflect these changes. It's important to preserve user data that is already in the on-device database when an app update changes the database schema.

Room supports both automated and manual options for incremental migration. Automatic migrations work for most basic schema changes, but you might need to manually define migration paths for more complex changes.

Automated migrations

To declare an automated migration between two database versions, add an @AutoMigration annotation to the autoMigrations property in @Database:

// Database class before the version update.
@Database(
  version = 1,
  entities = [User::class]
)
abstract class AppDatabaseV1 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

// Database class after the version update.
@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration(from = 1, to = 2)
  ]
)
abstract class AppDatabaseV2 : RoomDatabase() {
  abstract fun userDao(): UserDao
}

Automatic migration specifications

If Room detects ambiguous schema changes and it can't generate a migration plan without more input, it throws a compile-time error and you must provide an AutoMigrationSpec implementation. Most commonly, this occurs when a migration involves one of the following:

  • Deleting or renaming a table.
  • Deleting or renaming a column.

You can use AutoMigrationSpec to give Room the additional information that it needs to correctly generate migration paths. Define a class that implements AutoMigrationSpec in your RoomDatabase class and annotate it with one or more of the following:

To use the AutoMigrationSpec implementation for an automated migration, set the spec property in the corresponding @AutoMigration annotation:

@Database(
  version = 2,
  entities = [User::class],
  autoMigrations = [
    AutoMigration (
      from = 1,
      to = 2,
      spec = MigrationSpec1To2::class
    )
  ]
)
abstract class AppDatabaseWithSpec : RoomDatabase() {
  abstract fun userDao(): UserDao
}

@RenameTable(fromTableName = "User", toTableName = "AppUser")
internal class MigrationSpec1To2 : AutoMigrationSpec

If your app needs to do more work after the automated migration completes, you can implement onPostMigrate. If you implement this function in your AutoMigrationSpec, Room calls it after the automated migration completes.

Manual migrations

If a migration involves complex schema changes, Room might not be able to generate an appropriate migration path automatically. For example, if you decide to split the data in a table into two tables, Room can't determine how to perform this split. In these situations, you must manually define a migration path by implementing a Migration class.

A Migration class explicitly defines a migration path between a startVersion and an endVersion by overriding the migrate function. Add your Migration classes to your database builder using the addMigrations function:

val MIGRATION_1_2 = object : Migration(1, 2) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("CREATE TABLE `Fruit` (`id` INTEGER, `name` TEXT, " +
      "PRIMARY KEY(`id`))")
  }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
  override suspend fun migrate(connection: SQLiteConnection) {
    connection.executeSQL("ALTER TABLE Book ADD COLUMN pub_year INTEGER")
  }
}

Room.databaseBuilder<ManualMigrationDatabase>(applicationContext, "database-name")
  .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
  .build()

When you define your migration paths, you can use automated migrations for some versions and manual migrations for others. If you define both an automated migration and a manual migration for the same version, then Room uses the manual migration.

Test migrations

Migrations are often complex, and an incorrectly defined migration can cause your app to crash. To preserve your app's stability, test your migrations. Room provides a room3-testing Maven artifact to assist with the testing process for both automated and manual migrations. For this artifact to work, you must first export your database's schema.

Export schemas

Room exports your database's schema information into a JSON file at compile time. The exported JSON files represent your database's schema history. Store these files in your version control system so that you can recreate lower versions of the database for testing and support automated migration generation.

Set schema location using Room Gradle Plugin

To specify the schema directory, apply the Room Gradle Plugin and use the room3 extension.

Groovy

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

room3 {
  schemaDirectory("$projectDir/schemas")
}

If your database schema differs based on the variant, flavor, or build type, you must specify different locations by using the schemaDirectory configuration multiple times, each with a variantMatchName as the first argument. Each configuration can match one or more variants based on simple comparison with the variant name.

Make sure these are exhaustive and cover all variants. You can also include a schemaDirectory() without a variantMatchName to handle variants not matched by any of the other configurations. For example, in an app with two build flavors demo and full and two build types debug and release, the following are valid configurations:

Groovy

room3 {
  // Applies to 'demoDebug' only
  schemaDirectory "demoDebug", "$projectDir/schemas/demoDebug"

  // Applies to 'demoDebug' and 'demoRelease'
  schemaDirectory "demo", "$projectDir/schemas/demo"

  // Applies to 'demoDebug' and 'fullDebug'
  schemaDirectory "debug", "$projectDir/schemas/debug"

  // Applies to variants that aren't matched by other configurations.
  schemaDirectory "$projectDir/schemas"
}

Kotlin

room3 {
  // Applies to 'demoDebug' only
  schemaDirectory("demoDebug", "$projectDir/schemas/demoDebug")

  // Applies to 'demoDebug' and 'demoRelease'
  schemaDirectory("demo", "$projectDir/schemas/demo")

  // Applies to 'demoDebug' and 'fullDebug'
  schemaDirectory("debug", "$projectDir/schemas/debug")

  // Applies to variants that aren't matched by other configurations.
  schemaDirectory("$projectDir/schemas")
}

Set schema location using annotation processor option

If you aren't using the Room Gradle plugin, set the schema location using the room.schemaLocation annotation processor option.

Gradle uses files in this directory as inputs and outputs for some Gradle tasks. For correctness and performance of incremental and cached builds, you must use Gradle's CommandLineArgumentProvider to inform Gradle about this directory.

First, copy the following RoomSchemaArgProvider class into your module's Gradle build file. The asArguments function in the sample class passes room.schemaLocation=${schemaDir.path} to KSP. If you're using KAPT and javac, change this value to -Aroom.schemaLocation=${schemaDir.path} instead.

Groovy

class RoomSchemaArgProvider implements CommandLineArgumentProvider {

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  File schemaDir

  RoomSchemaArgProvider(File schemaDir) {
    this.schemaDir = schemaDir
  }

  @Override
  Iterable<String> asArguments() {
    return ["room.schemaLocation=${schemaDir.path}".toString()]
  }
}

Kotlin

class RoomSchemaArgProvider(
  @get:InputDirectory
  @get:PathSensitive(PathSensitivity.RELATIVE)
  val schemaDir: File
) : CommandLineArgumentProvider {

  override fun asArguments(): Iterable<String> {
    return listOf("room.schemaLocation=${schemaDir.path}")
  }
}

Then configure the compile options to use the RoomSchemaArgProvider with the specified schema directory:

Groovy

ksp {
  arg(new RoomSchemaArgProvider(new File(projectDir, "schemas")))
}

Kotlin

ksp {
  arg(RoomSchemaArgProvider(File(projectDir, "schemas")))
}

Test a single migration

Before you can test your migrations, add the androidx.room3:room3-testing artifact to your test dependencies and add the location of the exported schema as an asset directory:

Groovy

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
}

dependencies {
    ...
    androidTestImplementation "androidx.room3:room3-testing:3.0.1"
}

Kotlin

android {
    ...
    sourceSets {
        // Adds exported schema location as test app assets if not using
        // the Room Gradle Plugin.
        getByName("androidTest").assets.srcDir("$projectDir/schemas")
    }
}

dependencies {
    ...
    testImplementation("androidx.room3:room3-testing:3.0.1")
}

The testing package provides a MigrationTestHelper class, which can read exported schema files. The package also implements the JUnit4 TestRule interface to manage created databases.

The following example demonstrates a test for a single migration:

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    @get:Rule
    val helper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrate1To2() = runTest {
        val connection = helper.createDatabase(1)
        // Database has schema version 1. Insert some data using SQL queries.
        // You can't use DAO classes because they expect the latest schema.
        connection.execSQL("INSERT INTO User (id, name) VALUES (1, 'John Doe')")
        connection.close()

        // Re-open the database with version 2 and provide MIGRATION_1_2
        val migratedConnection = helper.runMigrationsAndValidate(2, listOf(MIGRATION_1_2))

        // MigrationTestHelper automatically verifies the schema changes,
        // but you need to validate that the data was migrated properly.
        val hasData = migratedConnection.prepare("SELECT COUNT(*) FROM User").use {
          it.step()
          it.getLong(0) > 0
        }
        assertTrue("Expected data was not migrated", hasData)
        migratedConnection.close()
    }
}

Test all migrations

Although you can test a single incremental migration, you should include a test that covers all migrations defined for your app's database. This helps ensure that there's no discrepancy between a recently created database instance and an earlier instance that followed the defined migration paths.

The following example demonstrates a test for all defined migrations:

@RunWith(AndroidJUnit4::class)
class MigrationTest {
    private val TEST_DB = "migration-test"

    private val instrumentation = InstrumentationRegistry.getInstrumentation()

    // Array of all migrations.
    private val ALL_MIGRATIONS = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)

    @get:Rule
    val helper: MigrationTestHelper = MigrationTestHelper(
        instrumentation = instrumentation,
        databaseClass = MigrationDb::class,
        driver = AndroidSQLiteDriver(),
        file = instrumentation.targetContext.getDatabasePath(TEST_DB),
    )

    @Test
    fun migrateAll() = runTest {
        // Create earliest version of the database.
        val connection = helper.createDatabase(1)
        connection.close()

        // Create latest version of the database.
        val db = Room.databaseBuilder<AppDatabase>(instrumentation.targetContext, TEST_DB)
          .setDriver(AndroidSQLiteDriver())
          .addMigrations(*ALL_MIGRATIONS)
          .build()
        // Open the database, Room validates the schema once all migrations
        // execute.
        db.useReaderConnection { connection ->
          // Perform additional validation
        }

        db.close()
    }
}

Gracefully handle missing migration paths

If Room can't find a migration path to upgrade an existing database on a device to the current version, an IllegalStateException occurs. If it's acceptable to lose existing data when a migration path is missing, call the fallbackToDestructiveMigration builder function when you create the database:

Room.databaseBuilder<FallbackMigrationDatabase>(applicationContext, "database-name")
        .fallbackToDestructiveMigration()
        .build()

This function configures Room to destructively recreate the tables in your app's database when it needs to perform an incremental migration and there's no defined migration path.

To fall back to destructive recreation only in certain situations, use one of the following alternatives to fallbackToDestructiveMigration:

  • If specific versions of your schema history cause errors that you can't solve with migration paths, use fallbackToDestructiveMigrationFrom instead. This function indicates that you want Room to fall back to destructive recreation only when migrating from specific versions.
  • If you want Room to fall back to destructive recreation only when migrating from a higher database version to a lower one, use fallbackToDestructiveMigrationOnDowngrade instead.