Room auto-migrations を試す

Room 2.4.0-alpha01 で(個人的に)待望の auto-migrations 機能がリリースされました。

SQLiteスキーマを変更する場合には、これまでMigrationクラスを実装する必要がありましたが、auto-migrationsによりマイグレーションの更新クエリがビルド時に自動的に生成されるようになります。

medium.com

サンプル実装

このようなテーブルをベースにします。

@Entity
data class Books(
    @PrimaryKey val title: String,
    val author: String
)

kaptでコードが生成されさえすればいいので、Databaseは最低限の実装で。

@Database(
    version = 1,
    exportSchema = true,
    entities = [
        Books::class
    ]
)
abstract class AppDatabase : RoomDatabase()

ビルドすると/app/build/generated/source/kapt/debug配下にAppDatabase_Impl.javaが生成されます。まだ auto-migration の指定をしていないので以下のような実装が見えます。

@Override
protected List<Migration> getAutoMigrations() {
  return Arrays.asList();
}

カラム追加

Booksテーブルにカラムを追加します。

 @Entity
 data class Books(
     @PrimaryKey val title: String,
-    val author: String
+    val author: String,
+    @ColumnInfo(defaultValue = "0") val page: Int
 )

auto-migrationでカラム追加を行う場合、デフォルト値を指定してあげないと以下のようなビルドエラーが発生しました(Kotlinのデフォルト引数でも同様でした)。

New NOT NULL column'page' added with no default value specified. Please specify the default value using @ColumnInfo.

AppDatabaseのバージョンをインクリメントして、autoMigrationsを指定します。

 @Database(
-    version = 1,
+    version = 2,
     exportSchema = true,
     entities = [
         Books::class
+    ],
+    autoMigrations = [
+        AutoMigration(from = 1, to = 2)
     ]
 )
 abstract class AppDatabase : RoomDatabase()

ビルドすると先ほど空リストを返していた実装が以下のようになっています。

@Override
protected List<Migration> getAutoMigrations() {
  return Arrays.asList(new AutoMigration_1_2_Impl());
}

Migrationクラスはこんな実装でした。

@SuppressWarnings({"unchecked", "deprecation"})
class AutoMigration_1_2_Impl extends Migration {
  public AutoMigration_1_2_Impl() {
    super(1, 2);
  }

  @Override
  public void migrate(@NonNull SupportSQLiteDatabase database) {
    database.execSQL("ALTER TABLE `Books` ADD COLUMN `page` INTEGER NOT NULL DEFAULT 0");
  }
}

カラム名変更

Roomが自動で変更検知できないマイグレーションの例としてカラム名変更を見てみます。

 @Entity
 data class Books(
-    @PrimaryKey val title: String,
+    @PrimaryKey val name: String,
     val author: String,
     @ColumnInfo(defaultValue = "0") val page: Int
 )

AppDatabaseはこのように変更します。

     autoMigrations = [
-        AutoMigration(from = 1, to = 2)
+        AutoMigration(from = 1, to = 2),
+        AutoMigration(from = 2, to = 3, spec = AppDatabase.BooksMigrationSpec::class)
     ]
 )
-abstract class AppDatabase : RoomDatabase()
+abstract class AppDatabase : RoomDatabase() {
+
+    @RenameColumn(tableName = "Books", fromColumnName = "title", toColumnName = "name")
+    class BooksMigrationSpec : AutoMigrationSpec
+}

ビルドするとAutoMigration_2_3_Impl.javaが生成されます。migrationメソッドだけ抜き出すとこんな感じです。

@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
  database.execSQL("CREATE TABLE IF NOT EXISTS `_new_Books` (`name` TEXT NOT NULL, `author` TEXT NOT NULL, `page` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))");
  database.execSQL("INSERT INTO `_new_Books` (author,name,page) SELECT author,title,page FROM `Books`");
  database.execSQL("DROP TABLE `Books`");
  database.execSQL("ALTER TABLE `_new_Books` RENAME TO `Books`");
  callback.onPostMigrate(database);
}
  1. rename後のカラム名_new_Booksテーブルを作成
  2. 既存データを移行
  3. 既存のBooksテーブルを削除
  4. _new_BooksBooksにrename

という流れになっています。これはhttps://sqlite.org/lang_altertable.html6. Making Other Kinds Of Table Schema Changesで解説されている方法と同じです。

実装

以下はコードリーディングのメモです。

だいたい次のようなステップでauto-migrationsのコード生成が実現されているようです。

  1. Databaseに指定したautoMigrations情報を取得
  2. それぞれのmigrationについて対象のスキーマの差分を検出し、差分の種類に応じたAutoMigrationに変換、通知する
  3. AutoMigrationの情報からjavapoetを使ってMigrationクラスを生成

autoMigrations情報を取得

AutoMigrationを生成

  • ↑のステップで得た情報を渡してAutoMigrationProcessorprocessメソッドを呼び出します
  • 先に(指定されていれば)AutoMigrationSpecを処理し、それぞれに対してAutoMigrationを作っていきます
  • これらの情報も考慮しつつ、対象スキーマの差分検出が行われます
    • 検出処理の実装はSchemaDiffer
    • 差分検出の実装は複雑です
    • 結果はSchemaDiffResultにまとめられて通知されます 
      • このクラスのプロパティを見ると、auto-migrationがスキーマの差分をどのように分類して処理をしているかがわかります
      • カラム名変更の場合はAutoMigration.ComplexChangedTableが作られます

Migrationクラスを生成

  • AutoMigrationWriterで行われます
  • SchemaDiffResultを見てjavapoetで頑張る実装です
  • migrationメソッドの生成はaddMigrationStatementsメソッドで行われています
  • AutoMigrationWriterを呼び出しているのはDatabaseProcessingStep

さいごに

stableリリースが待ち遠しいです。

使ったサンプルコードはこちらに置いています。

github.com