Room データベースを移行する

アプリの機能を追加または変更する場合、Room エンティティ クラスと基になるデータベース テーブルを編集して、そうした変更を反映させる必要があります。アプリのアップデートによってデータベース スキーマが変更される場合は、デバイス上のデータベースにある既存のユーザーデータを保持することが重要です。

Room は、増分移行について自動と手動の両方のオプションをサポートしています。自動移行はほとんどの基本的なスキーマ変更に対応しますが、より複雑な変更については手動で移行パスを定義する必要があります。

自動移行

2 つのデータベース バージョン間の自動移行を宣言するには、@DatabaseautoMigrations プロパティに @AutoMigration アノテーションを追加します。

// 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
}

自動移行の仕様

Room があいまいなスキーマ変更を検出し、さらに入力しないと移行計画を生成できない場合、コンパイル時エラーがスローされ、AutoMigrationSpec 実装を提供する必要があります。これは通常、移行に次のいずれかが伴う場合に発生します。

  • テーブルの削除または名前変更
  • 列の削除または名前変更

AutoMigrationSpec を使用すると、移行パスを正しく生成するために必要な追加情報を Room に提供できます。RoomDatabase クラスで AutoMigrationSpec を実装するクラスを定義し、次のうち 1 つ以上のアノテーションを付けます。

自動移行に AutoMigrationSpec 実装を使用するには、対応する @AutoMigration アノテーションで spec プロパティを設定します。

@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

自動移行の完了後にアプリでさらに作業を行う必要がある場合は、onPostMigrate を実装できます。AutoMigrationSpec に実装すると、この関数は自動移行の完了後に Room によって呼び出されます。

手動移行

移行に複雑なスキーマ変更が伴う場合、Room は適切な移行パスを自動的に生成できないことがあります。たとえば、1 つのテーブルのデータを 2 つのテーブルに分割する場合、Room にはこの分割の実行方法を判断できません。このような場合は、Migration クラスを実装して移行パスを手動で定義する必要があります。

Migration クラスは、migrate 関数をオーバーライドして、startVersionendVersion の間の移行パスを明示的に定義します。addMigrations 関数を使用して、Migration クラスをデータベース ビルダーに追加します。

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()

移行パスを定義するとき、あるバージョンでは自動移行を使用して、他のバージョンでは手動移行を使用できます。同じバージョンに自動移行と手動移行の両方を定義した場合、Room は手動移行を使用します。

移行をテストする

移行は多くの場合複雑なので、移行が正しく定義されていないとアプリがクラッシュする可能性があります。アプリの安定性を維持するには、移行をテストします。Room には、自動移行と手動移行の両方のテストプロセスを支援する room3-testing Maven アーティファクトが用意されています。このアーティファクトを機能させるには、まずデータベースのスキーマをエクスポートする必要があります。

スキーマをエクスポートする

Room はコンパイル時、データベースのスキーマ情報を JSON ファイルにエクスポートします。エクスポートされた JSON ファイルは、データベースのスキーマ履歴を表します。このファイルはバージョン管理システムに保存してください。そうすれば、テスト用に以前のバージョンのデータベースを再作成でき、自動移行の生成が可能になります。

Room Gradle プラグインを使用してスキーマの場所を設定する

スキーマ ディレクトリを指定するには、Room Gradle プラグインを適用し、room3 拡張機能を使用します。

Groovy

plugins {
  id 'androidx.room3'
}

room3 {
  schemaDirectory "$projectDir/schemas"
}

Kotlin

plugins {
  id("androidx.room3")
}

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

バリアント、フレーバー、ビルドタイプによってデータベース スキーマが異なる場合は、schemaDirectory 構成を複数回使用して、それぞれ variantMatchName を最初の引数として、異なる場所を指定する必要があります。各構成は、バリアント名との単純な比較に基づいて、1 つ以上のバリアントと一致させることができます。

これらが網羅的で、すべてのバリエーションをカバーしていることを確認します。variantMatchName のない schemaDirectory() を含めて、他の構成のいずれにも一致しないバリアントを処理することもできます。たとえば、2 つのビルド フレーバー demofull、2 つのビルドタイプ debugrelease を持つアプリでは、次の構成が有効です。

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")
}

アノテーション プロセッサ オプションを使用してスキーマの場所を設定する

Room Gradle プラグインを使用していない場合は、room.schemaLocation アノテーション プロセッサ オプションを使用してスキーマの場所を設定します。

Gradle は、このディレクトリ内のファイルを一部の Gradle タスクの入力と出力として使用します。増分ビルドとキャッシュ済みビルドの正確性とパフォーマンスを確保するには、Gradle の CommandLineArgumentProvider を使用して、このディレクトリを Gradle に通知する必要があります。

まず、次の RoomSchemaArgProvider クラスをモジュールの Gradle ビルドファイルにコピーします。サンプルクラスの asArguments 関数は room.schemaLocation=${schemaDir.path}KSP に渡します。KAPTjavac を使用している場合は、この値を -Aroom.schemaLocation=${schemaDir.path} に変更します。

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}")
  }
}

次に、指定されたスキーマ ディレクトリで RoomSchemaArgProvider を使用するようにコンパイル オプションを構成します。

Groovy

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

Kotlin

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

単一の移行をテストする

移行をテストする前に、androidx.room3:room3-testing アーティファクトをテストの依存関係に追加し、エクスポートしたスキーマの場所をアセット ディレクトリとして追加します。

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")
}

テスト パッケージには、エクスポートされたスキーマ ファイルを読み取ることができる MigrationTestHelper クラスが含まれています。パッケージは JUnit4 の TestRule インターフェースも実装しており、それを使用して、作成されたデータベースを管理できます。

次の例は、単一の移行をテストする方法を示しています。

@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()
    }
}

すべての移行をテストする

単一の増分移行をテストすることもできますが、アプリのデータベースに定義されているすべての移行をテストすることをおすすめします。そのようなテストにより、最近作成されたデータベース インスタンスと、定義済みの移行パスに沿って作成された古いインスタンスの間の不一致を解消できます。

次の例は、すべての定義済みの移行をテストする方法を示しています。

@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()
    }
}

移行パスの欠落を適切に処理する

デバイス上の既存のデータベースを現在のバージョンにアップグレードするための移行パスを Room が見つけられなかった場合、IllegalStateException が発生します。移行パスがない場合に既存のデータが失われることを許容できるのであれば、データベースの作成時に fallbackToDestructiveMigration ビルダー関数を呼び出します。

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

この関数は、増分移行を実行する必要があり、移行パスが定義されていない場合に、アプリのデータベース内のテーブルを破壊的に再作成するよう Room を構成します。

特定の状況でのみ代替手段として破壊的再作成を行いたい場合は、fallbackToDestructiveMigration の代わりとして以下の選択肢があります。

  • 移行パスで解決できないエラーの原因がスキーマ履歴の特定のバージョンにある場合は、fallbackToDestructiveMigrationFrom を使用します。この関数は、特定のバージョンから移行する場合にのみ代替手段として破壊的再作成を行うよう Room に指示します。
  • データベースの上位のバージョンから下位のバージョンに移行する場合にのみ、代替手段として破壊的再作成を行うよう Room に指示するには、fallbackToDestructiveMigrationOnDowngrade を使用します。