LoginSignup
8
9

More than 3 years have passed since last update.

Android Roomを使っていてカラム定義が変更したくなったら

Posted at

Roomのマイグレーションはいけてない

Google謹製の永続化ライブラリである『Room』を使用してAndroidアプリの開発をしているのですが、このRoomってやつ比較的新しいライブラリであるせいか、マイグレーションの機能がイケてないです。。。やりたかったことは「REAL型のカラムをINTEGER型に変更する」ってことだったのですが、sqliteの「ALTER文でできるのはカラムの追加だけ」という仕様と絶妙なマリアージュを醸し出し、実現するのに結構苦労しました・・・。この記事はそのメモです。

解決手順

先に解決手順から書きます。

  1. エンティティのプロパティの型を変更する(私の場合だとDouble?からInteger?に変更)
  2. 1.によって発生するビルドエラーに対応する
  3. @Databaseversionをインクリメントする
  4. マイグレーションコードを書く
    1. CREATE TABLE
    2. INSERT INTO ... SELECT
    3. DROP TABLE
    4. ALTER TABLE ... RENAME TO ...
    5. CREATE INDEX(インデックスがあれば)

このままだとあまりよく分からないと思いますので、各詳細を見ていきます。

1. エンティティのプロパティの型を変更する

例えば次のようなProductエンティティがあったとして、

Product.kt
@Entity(indices = [Index(value = ["name"])])
data class Product(
    @PrimaryKey(autoGenerate = true) var id: Int = 0,
    var name: String? = null,
    var price: Double? = null,
    ...

このpriceDouble?Int?に変更する、といったことです。
まあここは当然ですよね。

2. ビルドエラーに対応する

1.の変更で(普通は)ビルドエラーが発生するはずですので、ひとつずつ潰していきます。直し方は実装によるのでここでは割愛。

3. @Databaseversionをインクリメントする

たぶん、次のようにRoomDatabaseのサブ抽象クラスを定義してたりすると思います。

AppDatabase.kt
@Database(entities = [Product::class], version = 1)
abstract class AppDatabase : RoomDatabase() {

    companion object {

        @Volatile private var instance: AppDatabase? = null

        fun getInstance(context: Context): AppDatabase {
            return instance ?: synchronized(this) {
                instance ?: buildDatabase(context).also { instance = it }
            }
        }

        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "hogehoge-database")
                .build()
        }
    }
}

ここのversionを一つ上げます。この例でいくと2にするってことです。データベースのスキーマバージョンを2にしますよってことですね。

4. マイグレーションコードを書く

ここが一番面倒臭かった。Roomってマイグレーションに関してはほとんど何もしてくれません。結果的に生のSQLをゴリゴリ書く必要があります。sqliteの仕様で「カラムの変更、削除はできません」なので、やり方として、新しいテーブルを定義して、そこに現在のデータを移送してから旧テーブルを削除、その後新テーブルの名前を元通りにリネームしてやります。最後にこれが落とし穴なのですが、旧テーブル削除時にそのテーブルに張られていたインデックスも削除されるので、こいつを新テーブル生成後に作って上げないといけないということ。

まずCREATE TABLE文ですが、一から書くのはとても面倒だと思うので、Roomのスキーマ出力jsonを利用しましょう。app/build.gradleに次のような記述があると、ビルド時にスキーマ情報jsonを吐き出してくれます。

app/build.gradle
android {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
            }
        }

これでビルドすると、app/schemas配下にスキーマ情報がjsonとして出力されます。ファイル名は スキーマバージョン.json となっています。

このjsonをのぞいてみると、createSqlってキーの中にCREATE TABLE文が定義されていると思います。そいつをコピって、必要な変更を施して使うとラクです。

で、実際のマイグレーションコードですが、次のようになります。先程のAppDatabase.ktの例でいくと

AppDatabase.kt
...
        private fun buildDatabase(context: Context): AppDatabase {
            val migration1to2 = object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("CREATE TABLE IF NOT EXISTS `適当なテーブル名` ...")
                }
            }

            return Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "onieasy-database")
                .addMigrations(migration1to2)
                .build()
        }
...

スキーマバージョンについては実際のものに置き換えてください。

あとでリネームするので適当なテーブル名でいいです。
CREATE TABLEの次は、

  • INSERT INTO ... SELECT
  • DROP TABLE
  • ALTER TABLE ... RENAME TO

ですね。

            val migration1to2 = object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("CREATE TABLE IF NOT EXISTS 適当なテーブル名 ...")
                    database.execSQL("INSERT INTO 適当なテーブル名 SELECT * FROM 変更したいテーブル名")
                    database.execSQL("DROP TABLE 変更したいテーブル名")
                    database.execSQL("ALTER TABLE 適当なテーブル名 RENAME TO 変更したいテーブル名")
                }
            }

最後に、もともとインデックスが張ってあったなら、スキーマjsonの中からコピってきましょう。indicesの中にcreateSqlがあるはずです。

                    database.execSQL("CREATE INDEX index_Product_name` ON `Product` (`name`)")

みたいな感じです。

ここまで一気にやってからビルドして、アプリを動かしてみます。正しく変更できていれば正常に動くはずです。

なんでこんなややこしいねん

Roomはまだまだ発展途上なので、結構ハマりどころが多いと思います。最初私が思い付いたのは、Kotlinで新しいエンティティを定義して、その新エンティティにデータを移送すればいいんじゃないかということです。試してみたんですが、結果はアプリ起動せず・・・なんと、Roomはスキーマバージョン1の場合(つまり最初の最初)だけはエンティティからテーブルを生成してくれますが、以降はテーブルを自動生成してくれないのです! なんやねんそれ・・・

というわけでRoomを使う場合は、最初からきっちりテーブル設計しておかないと、あとあと面倒になるということですね、はい、勉強になりました。

8
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
9