Roomのマイグレーションはいけてない
Google謹製の永続化ライブラリである『Room』を使用してAndroidアプリの開発をしているのですが、このRoomってやつ比較的新しいライブラリであるせいか、マイグレーションの機能がイケてないです。。。やりたかったことは「REAL型のカラムをINTEGER型に変更する」ってことだったのですが、sqliteの「ALTER文でできるのはカラムの追加だけ」という仕様と絶妙なマリアージュを醸し出し、実現するのに結構苦労しました・・・。この記事はそのメモです。
解決手順
先に解決手順から書きます。
- エンティティのプロパティの型を変更する(私の場合だとDouble?からInteger?に変更)
- 1.によって発生するビルドエラーに対応する
-
@Database
のversion
をインクリメントする - マイグレーションコードを書く
- CREATE TABLE
- INSERT INTO ... SELECT
- DROP TABLE
- ALTER TABLE ... RENAME TO ...
- CREATE INDEX(インデックスがあれば)
このままだとあまりよく分からないと思いますので、各詳細を見ていきます。
1. エンティティのプロパティの型を変更する
例えば次のようなProduct
エンティティがあったとして、
@Entity(indices = [Index(value = ["name"])])
data class Product(
@PrimaryKey(autoGenerate = true) var id: Int = 0,
var name: String? = null,
var price: Double? = null,
...
このprice
のDouble?
をInt?
に変更する、といったことです。
まあここは当然ですよね。
2. ビルドエラーに対応する
1.の変更で(普通は)ビルドエラーが発生するはずですので、ひとつずつ潰していきます。直し方は実装によるのでここでは割愛。
3. @Database
のversion
をインクリメントする
たぶん、次のようにRoomDatabase
のサブ抽象クラスを定義してたりすると思います。
@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を吐き出してくれます。
android {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
これでビルドすると、app/schemas
配下にスキーマ情報がjsonとして出力されます。ファイル名は スキーマバージョン.json となっています。
このjsonをのぞいてみると、createSql
ってキーの中にCREATE TABLE文が定義されていると思います。そいつをコピって、必要な変更を施して使うとラクです。
で、実際のマイグレーションコードですが、次のようになります。先程の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を使う場合は、最初からきっちりテーブル設計しておかないと、あとあと面倒になるということですね、はい、勉強になりました。