k-shinn’s 雑記

技術メモを書き溜められたら良いな

RoomのMigrationテストで詰まった話

最近、AndroidにてRoomのMigrationテストを今更ながら実運用として書き始めました。 Room自体もMigrationテストも公式ドキュメントに沿えば十分書けるのですが、個人的に詰まった箇所が数カ所あるので、覚書として纏めたいと思います。

Roomの導入については以下の公式ドキュメントから参照すると良いと思います。

developer.android.com

Migrationとテストの手順については以下の箇所を参照すれば大丈夫です。

developer.android.com

exportSchemaの設定見直し

Migrationテストにはデータベースのバージョンごとのスキーマ履歴が必要になります。 これの出力設定は公式ドキュメントでは以下の箇所です。

私はRoom導入時にはMigrationテストの準備まで行っていなかったので、以下の様に exportSchemafalse にしてしばらく動かしていました。

@Database(
    entities = [UserEntity::class],
    version = 1,
    exportSchema = false
)
abstract class MyDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    ...

逆に言うと、この設定だとgradleの以下の記述は必要ありませんでした。

android {
    ...

    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments += [
                        "room.schemaLocation"  : "$projectDir/schemas".toString(),
                        "room.incremental"     : "true",
                        "room.expandProjection": "true"]
            }
        }
    }

    sourceSets {
        // Adds exported schema location as test app assets.
        androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
    }
    ...

とはいえ大した行数ではないので、導入処理としてひとまず一通り書いておいた方が良いだろうとは思います。

なお、exportSchema をそもそも書かないと、build時に以下の警告が出ることがあります。 記述時に警告が出ないので、忘れる事もあり得るかと思います。

警告: Schema export directory is not provided to the annotation processor so we cannot export the schema. You can either provide `room.schemaLocation` annotation processor argument OR set exportSchema to false.

Schemaファイルの活用

Migration設定を書く際に、以下の様に差分のSQLを書かなくてはいけません。

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL("CREATE TABLE IF NOT EXISTS `messages` (`id` INTEGER NOT NULL, `message` TEXT NOT NULL, PRIMARY KEY(`id`))")
    }
}

SQLに慣れている人には苦も無いことかも思いますが、私含め自信が無い人は、前述の手順で出力設定したスキーマ履歴のファイルを活用できます。

出力されたjsonファイルの中身を見ると、以下の様にテーブル作成のクエリがそのまま存在します。

    ...
    "entities": [
      {
        "tableName": "users",
        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`id`))",
        "fields": [
        ...

もちろん変数部分を書き換える必要はありますが、ほぼそのままMigration用のSQL文として流用できます。(この点を考えてもSchemaの出力設定は一先ずしておくべきだなと思いました)

androidTestとtestBuildType設定

RoomのMigrationテストはandroidTest側に書かなければなりません。…これ自体も、癖でローカルユニットテストとして書いてしまって、最初は動かずに首を傾げる自体になりました。

これはRoomのテストに限らない話なのですが、androidTestはbuildVariantを変更していると動かない場合があります。 以下のドキュメントに書いてある通りなのですが、デフォルトの debug 以外のbuildVariantに設定しているとErrorになってしまうので、注意が必要です。

この設定は、androidTestを開発フロー中のどの時点で、どの程度の頻度で行うかなど、プロジェクトの方針やCI設計に依存するものかと思います。 これは私も現在進行形で悩んでいることなので、良い方針ができれば別途CIの話として纏めたいと思います。

versionを前後しているとErrorする問題

これは開発上どうしようもない話かと思うのですが、開発中にデータベースのバージョンが違うブランチを行ったり来たりしていると、実行時にアプリが落ちることがあります。

例として、version2->1と移行してしまうと以下のExceptionが発生します。

 java.lang.IllegalStateException: A migration from 2 to 1 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow for destructive migrations via one of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

2->1 のMigrationなんて早々書かないとは思いますので、可能なら以下のドキュメントに沿って、 fallbackToDestructiveMigration を指定しておくと良いかと思います。

ただしこれは、ドキュメントにもある通り 既存のデータが失われることを許容できるのであれば という条件が付きます。 実運用でもMigrationに失敗してユーザのデータベースが失われても許容可能であるなら、開発効率にも影響してしまうのでやるべきかとは思います(そうならないためのMigrationテストではあるのですが…)。

なんにせよ、データベースはテストをちゃんと書いて、慎重に扱いたいものです。