この度、Androidアプリ「書籍管理ができる巻数メモ」を刷新しました!
刷新する際にあたり、既にリリースされているアプリであるため、アップデート後もDBデータを引き継いで使えるようにする必要があるため、マイグレーションする必要がありました。
そこで、DBデータをSQLiteOpenHelperからRoomライブラリの利用に変更するにあたり、必要となった処理について書いていきたいと思います。
SQLiteOpenHelperでのテーブル定義
刷新前のSQLiteOpenHelperクラスを継承してDBを定義していた時は以下のような実装となっていました。
class ListDataOpenHelper(context: Context?) : SQLiteOpenHelper(context, "ComicMemoDB", null, 2) {
override fun onCreate(db: SQLiteDatabase) {
db.execSQL(
"create table comicdata("
+ "id long not null,"
+ "title text default '',"
+ "author text default '',"
+ "number text default '1',"
+ "memo text default '',"
+ "inputdate text default '',"
+ "status long default 0"
+ ");"
)
}
Roomでのテーブル定義
Roomライブラリを利用してDBテーブルの定義をする場合には、以下のようにapp/build.gradleに追加します。
dependencies {
def room_version = "2.2.0"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
そして、データエンティティクラスを定義する事になり、それがそのままDBテーブルの定義となります。
データエンティティは以下のように定義しました。
@Entity(tableName = "comicdata")
data class Comic(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id") var id: Long,
@ColumnInfo(name = "title", defaultValue = "") var title: String,
@ColumnInfo(name = "author", defaultValue = "") var author: String,
@ColumnInfo(name = "number", defaultValue = "1") var number: String,
@ColumnInfo(name = "memo", defaultValue = "") var memo: String,
@ColumnInfo(name = "inputdate", defaultValue = "") var inputdate: String,
@ColumnInfo(name = "status", defaultValue = "0") var status: Long,
) : Serializable
マイグレーション時の問題点
各カラム名、データ型、デフォルト値も合わせているため、一見大丈夫そうに見えます。ですが、これでは自動マイグレーションを行えず、Exceptionが発生してしまいました。以下に問題点を列挙していきます。
1.java.lang.RuntimeException
Caused by: java.lang.RuntimeException: cannot find implementation for com.highcom.comicmemo.datamodel.ComicMemoRoomDatabase. ComicMemoRoomDatabase_Impl
というエラーが発生しました。これの原因としては、app/build.gradleにimplementationするライブラリが足りていないためでした。
以下を追加することで対処できました。
dependencies {
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
2.DELETE query methods must either return void or int (the number of deleted rows).
上記エラーは、DBテーブルを操作するためのデータアクセスオブジェクトクラス内で、@Queryアノテーション内でDELETEクエリを利用しているからでした。以下のように、Roomのバージョンを上げる事で対処しました。
dependencies {
def room_version = "2.4.2"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
3.java.lang.IllegalStateException: Migration didn't properly handle
これが一番厄介でした。各カラム名、データ型、デフォルト値を合わせているので一見問題なさそうに見えます。
ですが、実は合っていなかったのは以下の点でした。
・エンティティで定義するLong型はSQLiteのDBテーブルとしてはINTEGERとして定義される。
そもそもですが、SQLiteにはLong型は存在しないです。なので、移行前のcreate tableでは"id long not null,"と書いていますが、内部的にはINTEGERです。ですが、定義としては、longで定義していたので、型が合わずにエラーとなりました。
・エンティティで型定義する場合にはnull許容型にしない限り、NOT NULL制約が自動でつく。
よく考えれば当然のことでしたが、移行前のcreate tableでは"status long default 0"としていました。デフォルト値を定義しているのでnullになることは無いのですが、NOT NULL制約をつけていなかったので、移行後の型と合わずにエラーとなりました。
マイグレーションの実装
上記の問題点を踏まえて、マイグレーションの実装は以下となります。
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
database.beginTransaction()
try {
// 新しいテーブルを一時テーブルとして構築
database.execSQL("""
CREATE TABLE comicdata_tmp(
id INTEGER PRIMARY KEY NOT NULL,
title TEXT NOT NULL DEFAULT '',
author TEXT NOT NULL DEFAULT '',
number TEXT NOT NULL DEFAULT '1',
memo TEXT NOT NULL DEFAULT '',
inputdate TEXT NOT NULL DEFAULT '',
status INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
// 旧テーブルのデータを全て一時テーブルに追加
database.execSQL("""
INSERT INTO comicdata_tmp (id,title,author,number,memo,inputdate,status)
SELECT id,title,author,number,memo,inputdate,status FROM comicdata
""".trimIndent()
)
// 旧テーブルを削除
database.execSQL("DROP TABLE comicdata")
// 新テーブルをリネーム
database.execSQL("ALTER TABLE comicdata_tmp RENAME TO comicdata")
database.setTransactionSuccessful()
} finally {
database.endTransaction()
}
}
}
一旦、comicdata_tmpという名前のLong型→INTEGER型、NOT NULL制約を合わせたテーブルを作成します。そこに、既存のテーブルデータをINSERTして移行後、テーブル名をcomicdataに戻します。
こうすることで、SQLiteOpenHelperで作ったDBテーブルがRoomでデータを引き継いで利用する事が出来るようになります。
GitHubでのソースコードの公開
最後になりますが、上記の実装を行ったAndroidアプリをぜひダウンロードして利用してみて下さい!
また、上記内容も含んだ、ソースコードもGitHubで公開していますので、こちらも参考にしてもらえればと思います!