【Android】Realm Kotlin SDK 備忘録

# はじめに
初投稿です。なるべく正確な情報をまとめていこうと思いますが、まともにアプリを作ったのはこれが初めてなのでところどころ知識不足が露呈するかもしれません。わかりにくい所や不明確な点など、どんどん指摘してもらえたら嬉しいです。
最近Androidアプリの勉強を始め、Realm Kotlin SDKについて調べていたのですが、Android開発ではJavaSDKのほうが多用されており、またKotlinSDK自体がかなり新しめということで情報があまりないです。日本語の記事が全然見当たらなかったので自分でまとめてみようと思いました。TODOリストのサンプルをもとに進めていきます。
# Realm Kotlin SDKとは
モバイル向けの軽量なデータベースであるRealmをベースにしたKotlin用SDKです。Kotlinとの相性が良かったりKotlin MultiPlatformで使ったり出来ます。
# モデルクラス
Realmにデータを保存するには、まずデータの形式を定義したクラスを定義する必要があります。

- モデルクラスはRealmObjectインターフェースを実装する必要があります。
- モデルクラスは引数が無いコンストラクタを用意する必要があります。
- フィールドはすべてミュータブルにする必要があります。
- クラスの各フィールドにアノテーションをつけることで保存時の動作を変更できます。

|アノテーション|動作|
|:-----------:|:--:|
|PrimaryKey|オブジェクトにメインキーを設定|
|Index|フィールドにインデックスを設定(?)|
|Ignore|保存時にフィールドを無視|

- Realmに保存できる型は以下の型のみです。
    - String
    - Byte
    - Char
    - Short
    - Int
    - Long
    - Boolean
    - Double
    - Float
    - Decimal128
    - ObjectId
    - RealmInstant
    - RealmUUID
    - 任意のRealmObjectサブクラス
    - RealmList<T>
    - RealmSet<T>
    - RealmDictionary<T>

詳しくは[Realmの公式ドキュメント](https://www.mongodb.com/docs/realm/sdk/kotlin/realm-database/schemas/supported-types/)で確認してみてください。
以下はモデルクラスの例です。
```kotlin
class Task(): RealmObject {
    @PrimaryKey
    var uuid: RealmUUID = RealmUUID.random()
    var name: String = ""
    var isFinished: Boolean = false

    constructor(name: String = ""): this() {
        this.name = name
    }
}
```
Primaryアノテーションで保存データを読み込むためにキーとして使うフィールドを指定しています。

# RealmでのCRUD
いよいよRealmにデータを保存していきます。まず、Realmを開く処理が必要になります。
```kotlin
val config = RealmConfiguration.create(schema = setOf(Task::class))
val realm: Realm = Realm.open(config)
```
ここで生成したRealmオブジェクトを使って、データの追加や取得などを行います。

- データの読み取り
```kotlin
realm.writeBlocking{
    this.query<Task>("uuid == $0", uuid).find().first()
}
```
ここでは、PrimaryKeyアノテーションを用いて設定したメインキーを用いて、第二引数と一致したものを取得しています。第一引数はフィルタを指定するための文字列で、RealmQueryLanguageというフォーマットに則って書く必要があるみたいです。詳しくは[RealmQueryLanguage](https://www.mongodb.com/docs/realm/realm-query-language/#std-label-rql)のドキュメントを見てみてください。

- データの更新
```kotlin
realm.writeBlocking{
    val task = this.query<Task>("uuid == $0", uuid).find().first()
    task.name = "UpdatedName"
}
```

- データの削除
```kotlin
realm.writeBlocking{
    val task = this.query<Task>("uuid == $0", uuid).find().first()
    delete(task)
}
```

# 更新の検知
Realmではデータが更新された場合、それを検知する仕組みが用意されています。ここでは、データのすべての変更(追加、更新、削除)をキャッチするコードを例にとります。
```kotlin
val tasks = realm.query<Task::class>().find()
val job = CoroutineScope(Dispatchers.Default).launch {
    tasks.asFlow().collect {
        when (it) {
            is UpdatedResults -> { //更新時
                it.list //すべてのデータのリストを取得
            }
            is InitialResults -> //初回起動時の処理
        }
    }
}
```
queryの引数でフィルタを設定することで、フィルタにマッチした要素の更新のみを検知することも出来ます。また、itにはどの範囲のデータが更新されたかなどの情報も含まれています。更新の検知を解除したい場合は、大元のCoroutineをキャンセルしてあげればOKです。

# サポートされた型以外のデータの保存

Realmに保存できる型以外を扱いたいときがあるかもしれません。ここでは、Todoリストのタスクに期限を表すLocalDate型のフィールドを持たせたい場合を考えてみます。
結論から言うと、Kotlinのカスタムゲッター、セッターを使うことで擬似的に様々な型のデータを保存することが出来ます。
```kotlin
class Task(): RealmObject {
    @PrimaryKey
    var uuid: RealmUUID = RealmUUID.random()
    var name: String = ""
    var isFinished: Boolean = false
    private var _date: Long = LocalDate.now().toEpochDay()
    var date: LocalDate
        get() { return LocalDate.ofEpochDay(_date) }
        set(value) { _date = value.toEpochDay() }

    constructor(name: String = ""): this() {
        this.name = name
    }
}
```
privateな_dateフィールドにEpoch日を保持させ、実際にアクセスしたいときはdateフィールドを用いて、_dateからLocalDateを生成したり、受け取ったLocalDateの情報をもとに_dateフィールドの値を書き換えたりします。
また、デフォルトでRealmが保存できる型以外の型を持つフィールドは＠Ignoreをつけなくても無視されます。

# RealmObjectを入れ子する。
RealmObjectの中に別のRealmObjectを保存する場合、通常のようにRealmObjectを実装したサブクラスを作ると上手く行きませんでした。RealmObjectを入れ子する場合、子のRealmObjectはEmbeddedRealmObjectを継承する必要がありました。タスクの中にサブタスクを追加する場合を考えます。
```kotlin
class SubTask(): EmbeddedRealmObject {
    var name: String = ""
    var isFinished: Boolean = false

    constructor(name: String, isFinished: Boolean): this(){
        this.name = name
        this.isFinished = isFinished
    }
}

class Task(): RealmObject {
    @PrimaryKey
    var uuid: RealmUUID = RealmUUID.random()
    var name: String = ""
    var isFinished: Boolean = false
    var subTasks: RealmList<SubTask> = realmListOf()

    constructor(
        uuid: RealmUUID = RealmUUID.random(),
        name: String = "",
        isFinished: Boolean = false,
        subTasks: RealmList<SubTask> = realmListOf()
        ): this() {
        this.uuid = uuid
        this.name = name
        this.isFinished = isFinished
        this.subTasks = subTasks
    }
}
```
また、Realmを開く際に、schemaに子要素のクラスを指定する必要があります。
```kotlin
val config = RealmConfiguration.create(schema = setOf(Task::class, SubTask::class))
val realm: Realm = Realm.open(config)
```
注意点として、EnbeddedRealmObjectはPrimaryKeyを持つことが出来ません。また、親のデータが削除された場合自動的に子の要素も削除されるようです。要素の更新や削除は、先程と同様に親要素を取得してから行うことでできます。

# 最後に
まだまだ知識が浅いというのもあり、かなり分かりづらい文章になってしまったと思いますが、最後まで見ていただいてありがとうございます。不正確な点などは随時更新していこうと思うので、遠慮なく指摘してもらえたら嬉しいです。