LoginSignup
9
6

More than 3 years have passed since last update.

RoomでMigration

Posted at

検証環境

この記事の内容は、以下の環境で検証しています。
* 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

DB変更前のソースコード

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

クラス 名or ファイル名 役割
MyEntity ・エンティティ
・テーブル定義
MyDao @Daoアノテーション付与したDAOの定義
MyDatabase ・RoomDatabaseを継承したDatabaseクラス
・Room.BuilderでRoomDatabaseクラスをビルド
Contextクラスのプロパティを追加し、RoomDatabaseのオブジェクトを格納する

※Activityやbuild.gradle、レイアウトファイルは省略します。

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

befor_migration.png

Entity

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

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メソッドを呼び出しています。

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)

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

公式サイトを確認すると、下記のような手順が書かれています。

androidx.room.migration.Migration という、抽象クラスを実装する。migrateメソッド内にテーブルを変更するDDLを実行する。
RoomDatabase.BuilderクラスのaddMigrationsメソッドで実装したMigration抽象クラスのオブジェクトを渡す。

早速、実装してみました。やったことは、下記の3点です。

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

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

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メソッドの追記

Room.databaseBuilder(context, MyDatabase::class.java, "data").apply {
    allowMainThreadQueries()
    addMigrations(MIGRATION_1_2)
}.build()

addMigrationsメソッドを追加して、先程定義したMIGRATION_1_2を渡しています。
これで、ファイルのバージョンに差分が発生すると呼び出されます。
メソッドの定義を見るとpublic Builder addMigrations (Migration... migrations)となっているので、複数のバージョンのMigrationを渡しても問題なさそうですね。

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

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

@Database(entities = [MyEntity::class, Hobby::class], version = 2, exportSchema = false)
@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

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

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

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

DB変更後のソースコード

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

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

MyEntity.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,
    val age:Int?
)

Hobby.kt

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

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

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書くんだと思いつつ、注意点が多くあることがわかりました。
気をつけて実装していきましょう。

9
6
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
9
6