Room 2.4.0-alpha01 で(個人的に)待望の auto-migrations 機能がリリースされました。
SQLiteのスキーマを変更する場合には、これまでMigration
クラスを実装する必要がありましたが、auto-migrationsによりマイグレーションの更新クエリがビルド時に自動的に生成されるようになります。
サンプル実装
このようなテーブルをベースにします。
@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); }
- rename後のカラム名で
_new_Books
テーブルを作成 - 既存データを移行
- 既存の
Books
テーブルを削除 _new_Books
をBooks
にrename
という流れになっています。これはhttps://sqlite.org/lang_altertable.htmlの6. Making Other Kinds Of Table Schema Changes
で解説されている方法と同じです。
実装
以下はコードリーディングのメモです。
だいたい次のようなステップでauto-migrationsのコード生成が実現されているようです。
- Databaseに指定した
autoMigrations
情報を取得 - それぞれのmigrationについて対象のスキーマの差分を検出し、差分の種類に応じた
AutoMigration
に変換、通知する AutoMigration
の情報からjavapoetを使ってMigration
クラスを生成
autoMigrations情報を取得
- DatabaseProcessorというクラスにある
processAutoMigrations
メソッドで行われています - マイグレーション対象のスキーマは.jsonからSchemaBundleというクラスにデシリアライズされています
SchemaBundle
のdeserialize
メソッドで処理されていますが、Gsonが使われているようです
AutoMigrationを生成
- ↑のステップで得た情報を渡してAutoMigrationProcessorの
process
メソッドを呼び出します - 先に(指定されていれば)
AutoMigrationSpec
を処理し、それぞれに対してAutoMigration
を作っていきます - これらの情報も考慮しつつ、対象スキーマの差分検出が行われます
- 検出処理の実装はSchemaDiffer
- 差分検出の実装は複雑です
- 結果は
SchemaDiffResult
にまとめられて通知されます
Migrationクラスを生成
- AutoMigrationWriterで行われます
SchemaDiffResult
を見てjavapoet
で頑張る実装ですmigration
メソッドの生成はaddMigrationStatements
メソッドで行われていますAutoMigrationWriter
を呼び出しているのはDatabaseProcessingStep
さいごに
stableリリースが待ち遠しいです。
使ったサンプルコードはこちらに置いています。