検証環境
この記事の内容は、以下の環境で検証しています。
- Java:open jdk 1.8.0_152
- Kotlin 1.3.61
- Android Studio 3.5.3
- CompileSdkVersion:29
はじめに
Androidでデータベースを扱う際、Android Jetpackに含まれているRoomを使うことにより、標準APIで実装するより簡潔に実装が出来ます。
しかし、アプリを更新していくうちにテーブルの列を変更したり、テーブルを追加したりします。
その様な更新(マイグレーション)方法について説明した記事になっています。
DBの変更内容
この記事では数の様に、列の追加とテーブルの追加を行います。
※列やテーブルに特段の意味は有りません。あくまで一例と捉えてください。
DB変更前のソースコード
初めに、マイグレーションを行う前のソースコードを掲載します。
ER図と併せて確認しておくと、差分がよく分かると思います。
クラス 名or ファイル名 | 役割 |
---|---|
MyEntity | ・エンティティ ・テーブル定義 |
MyDao | ・@Daoアノテーション付与したDAOの定義 |
MyDatabase | ・RoomDatabaseを継承したDatabaseクラス ・Room.BuilderでRoomDatabaseクラスをビルド Contextクラスのプロパティを追加し、RoomDatabaseのオブジェクトを格納する |
※Activityやbuild.gradle、レイアウトファイルは省略します。 |
下記のコードを実行すると下記のようなテーブルが作成されます。
Entity
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
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メソッドを呼び出しています。
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の時に初めてインストールした時はどの様な動作をするかということです。
調べてみたところ、下図のような挙動をしています。
要するに、初めてインストールした人は、@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書くんだと思いつつ、注意点が多くあることがわかりました。
気をつけて実装していきましょう。