Java SDKで作られたRealmのデータをKotlin SDKでマイグレーションする
元々、アプリ内にデータを保存するためにJava SDKでRealmを使っていたのですが、アプリのアップデートでKotlin SDKに乗り換えることになりました
テーブルの内容も併せて変わるのでRealmのデータのマイグレーションが必要なのですが、いい感じにまとめられた記事が見かけられず、地味に詰まってしまったので、同志がいることを信じて残すことにします
ちなみに、公式には上記のような感じのものはなかった様な気がします
Java SDKからKotlin SDKに乗り換えるときのコードの書き換えみたいなのはありました
環境
Realm
Java SDK(ver:10.15.1
)からKotlin SDK(ver:1.11.0
)に乗り換えます
ちなみに、Kotlin SDK(ver:1.15.0
)で破壊的な変更があるので、これ以上のバージョンを使う場合は破壊的な変更を考慮する必要があります
Realm以外
Androidアプリです
知っておくべきこと
今回、自分が出くわしたところだけ抜粋しています
他にも知っておくべきことがあるかもしれませんが、割愛します
保存できるクラスの違い
まず、Java SDKとKotlin SDKでは、保存できるクラスが異なります
RealmObject
などのRealm専用のクラスを除外すると、Java SDKで使えるクラスとKotlin SDKで使えるクラスは以下になります
Java | Kotlin |
---|---|
Boolean or boolean
|
Boolean |
Integer or int
|
Int |
Short or short
|
Short |
Long or long
|
Long |
Byte or byte[]
|
Byte |
Double or double
|
Double |
Float or float
|
Float |
String |
String |
Date |
|
UUID from java.util.UUID
|
|
Char |
なので上記より、Java SDKからKotlin SDKに移行する場合、Date
とUUID
はデータの中をいじる必要がなくても、何らかのマイグレーション処理が必須になります
今回はDate
をマイグレーションする必要があり、一番つまづきました・・・
スキーマバージョンの管理
データベースエンジニアとかなら、ごく当たり前の内容だと思いますが、地味に詰まってしまったので一応書きます
マイグレーションする時、スキーマバージョンを気にする必要があるのですが、以下を満たす必要があります
- Longでスキーマバージョンを指定する
- マイグレーション前よりもスキーマバージョンを上げる
- 特に何も指定せずにRealmを使っていたら、スキーマバージョンは
0L
になります
- 特に何も指定せずにRealmを使っていたら、スキーマバージョンは
- クエリを発行する時、上げたスキーマバージョンを指定する
上記を守らないと、何かしらでエラーになります(スキーマバージョンがあっていないとか、古いスキーマバージョンだぞとかのエラーが出てくる)
Date
のマイグレーション
本題です
前述の通り、Date
をマイグレーションする場合、他のクラスに変換してマイグレーションする必要があります
サポートされているクラスに変換すればよいのですが、以下を満たすクラスに変換したいです
- 変換後のクラス自身も
Date
っぽく扱える -
Date
と変換先のクラスの変換処理が簡単に実装できる
そんなクラスがRealmの独自クラスに用意されています
RealmInstant
です
KotlinのInstant
のRealmバージョンです
今回はこれに変換して移行します
マイグレーションの基本的な方針は、「Date
をエポック秒に変換してRealmInstant
として保存する」です
具体的にどうすれば?
以下のようなRealmObject
を移行することを例として挙げてみます
// 移行元のRealmのテーブル
public class TransitionSourceTable extends RealmObject {
@PrimaryKey
String id;
Date date;
}
まずは、移行先のRealmObject
と、Date
とRealmInstant
を変換するDateAdapter
クラスを用意します
移行先のRealmObject
は、Date
のマイグレーション処理をシンプルにしたいため、date
以外のカラムとカラム数は基本的にTransitionSourceTable
と同じにします
DateAdapter
クラスはKotlin SDKのGitHubのIssueを元に実装しました
// 移行先のRealmのテーブル
class TransitionToTable : RealmObject {
// TransitionSourceTable.idの移行先カラム
// Kotlin SDKではデフォルト値がないとビルドエラーになるので、空文字を設定しています
@PrimaryKey
var id: String = ""
// TransitionSourceTable.dateの移行先カラム
var dateAtInstant: RealmInstant = RealmInstant.now()
// Ignoreアノテーションを使うことで、サポートされていないクラスの変数が定義できる
// Realm自体には保存されないが、TransitionSourceTableでコードを書いていたときの使用感を可能な限り再現したいので追加
@Ignore
var date: Date by DateAdapter(realmInstantProperty = TransitionToTable::dateAtInstant)
}
// DateとRealmInstantを変換するアダプタークラス
class DateAdapter(
private val realmInstantProperty: KMutableProperty<RealmInstant>,
) {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Date {
return Date(realmInstantProperty.call(thisRef).epochSeconds)
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Date) {
realmInstantProperty.setter.call(thisRef, value)
}
}
次に実際のマイグレーション処理を書きます
fun realmMigrate() {
val schemaVersion = 1L
val migrationConfig = RealmConfiguration.Builder(
// 移行先が複数あれば、setOfの中にクラスを追加する
schema = setOf(
TransitionSourceTable::class,
),
)
.schemaVersion(schemaVersion)
.migration(
{ migrationContext ->
// 移行するクラスが複数あれば、migrationContext.enumerate(className = "hogehoge") { oldObject, newObject ->}をその数だけ用意する
migrationContext.enumerate(className = "TransitionSourceTable") { oldObject, newObject ->
newObject?.run {
set(
"id",
oldObject.getNullableValue<String>("id"),
)
set(
"dateAtInstant",
oldObject.getNullableValue<RealmInstant>("date"),
)
}
}
},
)
.build()
Realm.open(migrationConfig)
}
// アプリ起動時などのタイミングで以下を実行
realmMigrate()
これでJava SDKで作成されたRealmのデータをKotlin SDKを用いて移行することが出来ます
本当に移行できているか確認したい
Realm Studioというツールで確認出来ます
*.realm
のデータの中身を見られるツールです
Androidアプリ内のRealmファイルの取得の仕方やRealm Studioの導入や使い方はマイグレーション処理からそれるので割愛します(ググったらこれらの記事がいっぱい出てきます)
その他
本題に関わりがないが、マイグレーション周りで知っておくと幸せになれるかもしれない部分をピックアップしました
AutomaticSchemaMigration
移行するカラムのクラスに変更がなく、データの中身もそのまま移行する場合、AutomaticSchemaMigration
を使うとより簡単にマイグレーション処理を各ことが出来ます
今回は使うことはなかったのですが、そのまま移行するカラムが多い場合はこちらを使うと良いかもしれないです
deleteRealmIfMigrationNeeded()
開発中、RealmObject
が頻繁に変わるたびにマイグレーション処理を適切に書かなければならなくなりますが、今はマイグレーション処理の実装じゃない場合が多々あります(機能実装したいときとか)
そういうときはdeleteRealmIfMigrationNeeded()
を呼び出すと、マイグレーションが必要になったとき、自動的に元々あったデータをすべて削除する事ができます
しかし、元々のRealmデータが残っているアプリが既にリリースされていて、アプリのアップデート後にこれらのデータを削除しないのであれば、実際のマイグレーション処理は必要になるので、お忘れなく・・・
株式会社 カラビナテクノロジーは「命綱や支点を素早く確実に繋ぐカラビナ。そんなカラビナのような役割をテクノロジーで実現したい」という想いのもと、福岡で設立。 主にシステム開発・アプリ開発・ Webサイト制作を行っています。採用情報→karabiner.tech/recruit/requirements/
Discussion