💽

Java SDKで作られたRealmのデータをKotlin SDKでマイグレーションする

2024/05/02に公開

元々、アプリ内にデータを保存するために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に移行する場合、DateUUIDはデータの中をいじる必要がなくても、何らかのマイグレーション処理が必須になります
今回はDateをマイグレーションする必要があり、一番つまづきました・・・

スキーマバージョンの管理

データベースエンジニアとかなら、ごく当たり前の内容だと思いますが、地味に詰まってしまったので一応書きます

マイグレーションする時、スキーマバージョンを気にする必要があるのですが、以下を満たす必要があります

  • Longでスキーマバージョンを指定する
  • マイグレーション前よりもスキーマバージョンを上げる
    • 特に何も指定せずにRealmを使っていたら、スキーマバージョンは0Lになります
  • クエリを発行する時、上げたスキーマバージョンを指定する

上記を守らないと、何かしらでエラーになります(スキーマバージョンがあっていないとか、古いスキーマバージョンだぞとかのエラーが出てくる)

Dateのマイグレーション

本題です
前述の通り、Dateをマイグレーションする場合、他のクラスに変換してマイグレーションする必要があります
サポートされているクラスに変換すればよいのですが、以下を満たすクラスに変換したいです

  • 変換後のクラス自身もDateっぽく扱える
  • Dateと変換先のクラスの変換処理が簡単に実装できる

そんなクラスがRealmの独自クラスに用意されています
RealmInstantです
KotlinのInstantのRealmバージョンです
今回はこれに変換して移行します
マイグレーションの基本的な方針は、「Dateをエポック秒に変換してRealmInstantとして保存する」です

具体的にどうすれば?

以下のようなRealmObjectを移行することを例として挙げてみます

TransitionSourceTable.java
// 移行元のRealmのテーブル
public class TransitionSourceTable extends RealmObject {
    @PrimaryKey
    String id;
    Date date;
}

まずは、移行先のRealmObjectと、DateRealmInstantを変換するDateAdapterクラスを用意します
移行先のRealmObjectは、Dateのマイグレーション処理をシンプルにしたいため、date以外のカラムとカラム数は基本的にTransitionSourceTableと同じにします
DateAdapterクラスはKotlin SDKのGitHubのIssueを元に実装しました

TransitionToTable.kt
// 移行先の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)
    }
}

次に実際のマイグレーション処理を書きます

Migrate.kt
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データが残っているアプリが既にリリースされていて、アプリのアップデート後にこれらのデータを削除しないのであれば、実際のマイグレーション処理は必要になるので、お忘れなく・・・

カラビナテクノロジー デベロッパーブログ

Discussion