RoomでMigration

# 検証環境
この記事の内容は、以下の環境で検証しています。
* Java:open jdk 1.8.0_152
* Kotlin 1.3.61
* Android Studio 3.5.3
* CompileSdkVersion:29

# はじめに
Androidでデータベースを扱う際、Android Jetpackに含まれているRoomを使うことにより、標準APIで実装するより簡潔に実装が出来ます。
しかし、アプリを更新していくうちにテーブルの列を変更したり、テーブルを追加したりします。
その様な更新（マイグレーション）方法について説明した記事になっています。

# DBの変更内容
この記事では数の様に、列の追加とテーブルの追加を行います。
※列やテーブルに特段の意味は有りません。あくまで一例と捉えてください。

![Migration_er_diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/1baa4ecd-4e9d-5175-3eef-38ef47ad1d6f.png)


# DB変更前のソースコード
初めに、マイグレーションを行う前のソースコードを掲載します。
ER図と併せて確認しておくと、差分がよく分かると思います。

|クラス 名or ファイル名|役割|
|:---|:---|
|MyEntity|・エンティティ<br>・テーブル定義|
|MyDao|・@Daoアノテーション付与したDAOの定義|
|MyDatabase|・RoomDatabaseを継承したDatabaseクラス<br>・Room.BuilderでRoomDatabaseクラスをビルド<br>Contextクラスのプロパティを追加し、RoomDatabaseのオブジェクトを格納する|
※Activityやbuild.gradle、レイアウトファイルは省略します。

下記のコードを実行すると下記のようなテーブルが作成されます。

![befor_migration.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/f43e003f-63b9-3ab2-1444-73dd4538cc1a.png)


## Entity

```kotlin:MyEntitiy.kt
package jp.co.roommigration.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class MyEntity(
    @PrimaryKey(autoGenerate = true)
    val id:Int?=null,
    val name:String
)
```



## MyDao

```kotlin:MyDao.kt
package jp.co.roommigration.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import jp.co.roommigration.entity.MyEntity

@Dao
interface MyDao {

    @Query("select * from MyEntity")
    fun selectAll():List<MyEntity>

    @Insert
    fun insert(vararg entities:MyEntity)
}
```

## MyDatabase
便宜上、メインスレッドでも実行できるように、ビルド時にallowMainThreadQueriesメソッドを呼び出しています。

```kotlin:MyDatabase.kt
package jp.co.roommigration.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import jp.co.roommigration.dao.MyDao
import jp.co.roommigration.entity.MyEntity

@Database(entities = [MyEntity::class], version = 1, exportSchema = false)
abstract class MyDatabase : RoomDatabase() {
    abstract fun mydao(): MyDao

    companion object {

        private var instance: MyDatabase? = null

        fun getInstance(context: Context): MyDatabase = instance ?: let {
            Room.databaseBuilder(context, MyDatabase::class.java, "data").apply {
                allowMainThreadQueries()
            }.build()
        }
    }
}

val Context.database: MyDatabase
    get() = MyDatabase.getInstance(applicationContext)

```

# マイグレーションしてみる

[公式サイト](https://developer.android.com/training/data-storage/room/migrating-db-versions)を確認すると、下記のような手順が書かれています。

>   **androidx.room.migration.Migration** という、抽象クラスを実装する。migrateメソッド内にテーブルを変更するDDLを実行する。
>  RoomDatabase.BuilderクラスのaddMigrationsメソッドで実装したMigration抽象クラスのオブジェクトを渡す。
 
早速、実装してみました。やったことは、下記の３点です。

* Migration抽象クラスを実装したMIGRATION_1_2の定義
* addMigrationsメソッドの追記
* @Databaseアノテーションのentities属性に新たに追加した@Entityアノテーションを付与したクラス情報の追加とMyEntityクラスの修正

### Migration抽象クラスを実装したMIGRATION_1_2の定義

```kotlin
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "CREATE TABLE Hobby (" +
                " id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                " name TEXT NOT NULL " +
                ");")

        database.execSQL("alter table MyEntity add age integer default 10;")

    }
}
```
コンストラクタの引数2つに、マイグレーションするデータベースファイルのバージョンを指定します。
今回は1から2までのマイグレーションになります。
maigrateメソッドでは、SupportSQLiteDatabaseのオブジェクトが渡されるので、execSQLメソッドを呼び出してDDLを実行しています。

### addMigrationsメソッドの追記

```kotlin
Room.databaseBuilder(context, MyDatabase::class.java, "data").apply {
    allowMainThreadQueries()
    addMigrations(MIGRATION_1_2)
}.build()
```
addMigrationsメソッドを追加して、先程定義したMIGRATION_1_2を渡しています。
これで、ファイルのバージョンに差分が発生すると呼び出されます。
メソッドの定義を見ると**public Builder<T> addMigrations (Migration... migrations)**となっているので、複数のバージョンのMigrationを渡しても問題なさそうですね。

### @Databaseアノテーションのentities属性に新たに追加した@Entityアノテーションを付与したクラス情報の追加とMyEntityクラスの修正

テーブルを追加するので、@Entityアノテーションを付与したクラスも追加しておきました。
もちろん、MyEntityテーブルに変更が発生してるので、MyEntityクラスの修正もわすれません。
※import文とpackageは省略

```kotlin
@Database(entities = [MyEntity::class, Hobby::class], version = 2, exportSchema = false)
```

```kotlin
@Entity
data class Hobby (
    @PrimaryKey(autoGenerate = true)
    val id:Int? = null,
    val name:String
)
```

```
@Entity
data class MyEntity(
    @PrimaryKey(autoGenerate = true)
    val id:Int?=null,
    val name:String,
    val age:Int?
)
```

実行してみると、たしかにマイグレーションは成功しています。
ここで気になることがあります。
データベースファイルのバージョンが2の時に初めてインストールした時はどの様な動作をするかということです。

調べてみたところ、下図のような挙動をしています。

![Migrationの挙動.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/da817d01-b931-bc6e-2b69-f07e88e14319.png)


要するに、初めてインストールした人は、**@Databaseアノテーションのentities属性に記述したクラスの定義でテーブルが作成され、addMigrationsメソッドは無視される**ということです。

そのため、マイグレーションで最も注意すべき事は、<font color="red">【Migration抽象クラスでテーブル変更した場合は、必ず@Entityアノテーションが付与されているクラスを**完全一致**させる必要がある】</font>です。

本当に注意が必要ですね。

## DB変更後のソースコード
最後に、修正が完了したソースコードの全貌を記載しておきます。

|クラス 名or ファイル名|役割|
|:---|:---|
|MyEntity|・エンティティ<br>・テーブル定義|
|<font color="red">Hobby</font>|<font color="red">・エンティティ<br>・テーブル定義<br>↑NEW</font>|
|MyDao|・@Daoアノテーション付与したDAOの定義（今回は変更なし）|
|MyDatabase|・RoomDatabaseを継承したDatabaseクラス<br>・Room.BuilderでRoomDatabaseクラスをビルド<br>Contextクラスのプロパティを追加し、RoomDatabaseのオブジェクトを格納する<br><font color="red">・addMigrationsメソッドでマイグレーションを設定する<br>↑NEW</font>|

### MyEntity.kt

```kotlin
package jp.co.roommigration.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class MyEntity(
    @PrimaryKey(autoGenerate = true)
    val id:Int?=null,
    val name:String,
    val age:Int?
)
```

### Hobby.kt

```kotlin
package jp.co.roommigration.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class Hobby (
    @PrimaryKey(autoGenerate = true)
    val id:Int? = null,
    val name:String
)
```

### MyDao.kt

```kotlin
package jp.co.roommigration.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import jp.co.roommigration.entity.MyEntity

@Dao
interface MyDao {

    @Query("select * from MyEntity")
    fun selectAll():List<MyEntity>

    @Insert
    fun insert(vararg entities:MyEntity)
}
```

### MyDatabase.kt

```kotlin
package jp.co.roommigration.db

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import jp.co.roommigration.dao.MyDao
import jp.co.roommigration.entity.Hobby
import jp.co.roommigration.entity.MyEntity

@Database(entities = [MyEntity::class, Hobby::class], version = 2, exportSchema = false)
abstract class MyDatabase : RoomDatabase() {
    abstract fun mydao(): MyDao

    companion object {

        private var instance: MyDatabase? = null

        fun getInstance(context: Context): MyDatabase = instance ?: let {
            Room.databaseBuilder(context, MyDatabase::class.java, "data").apply {
                allowMainThreadQueries()
                addMigrations(MIGRATION_1_2)
            }.build()
        }
    }
}

val Context.database: MyDatabase
    get() = MyDatabase.getInstance(applicationContext)

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "CREATE TABLE Hobby (" +
                " id INTEGER PRIMARY KEY AUTOINCREMENT, " +
                " name TEXT NOT NULL " +
                ");")

        database.execSQL("alter table MyEntity add age integer default 10;")

    }
}
```

# まとめ
Roomを使ったマイグレーションについて説明してきました。
マイグレーションはDDL書くんだと思いつつ、注意点が多くあることがわかりました。
気をつけて実装していきましょう。