Realmの初期データをKotlinで作る

プロジェクトでカタログアプリのようなものを作る機会がありました。
そこでは、商品の切り替えはあまりなく、あっても1年に1度程度、アプリのアップデートと絡めてで問題ないということでした。
そのため、Realmのデータベースを別途作成、それをiOS/Androidの両方のアプリに組み込む形としました。

単純なCSVからテーブルを作るだけであれば、Realm Studioの機能でできるのですが、ある程度正規化したりしながら作る場合、プログラムを書く必要があります。

Swiftでの初期データ作成は記事があったのですが、個人的にはAndroidの方が得意だったので、Kotlinでやってみました。

（Swiftでの記事: https://qiita.com/negibouze/items/0fe2928ea2f00d8d3cd3 ）

## 完成形

https://github.com/noboru-i/android-realm-creator

Androidエミュレータを起動し、DB作成用のアプリを実行、adbコマンドを利用してローカルにコピーする形です。
（本当は、Androidを利用せずにやりたかったのですが、単独のJava用SDKなどが見当たらなかったので、この形にしました。）

コマンドとしては、下記のとおりです。

```shell
./gradlew :realmcreator:installDebug
adb shell am start -n hm.orz.chaos114.android.realmcreator/hm.orz.chaos114.android.realmcreator.MainActivity
adb shell run-as hm.orz.chaos114.android.realmcreator cat files/output.realm > default.realm
```

取得できた `default.realm` を、利用したいアプリの `app/src/main/assets/` に配置し、下記のように読み込めます。

```kotlin
val config = RealmConfiguration.Builder()
        .name(Realm.DEFAULT_REALM_NAME)
        .assetFile("default.realm")
        .readOnly()
        .build()
realm = Realm.getInstance(config)
```

## 今回のサンプル

この前の世界バレーの結果を、下記のような形式のCSVにしたものを使います。

| POOL A | 日本 | オランダ | アルゼンチン | ドイツ | カメルーン | メキシコ |
|:--|:--|:--|:--|:--|:--|:--|
| 日本 | - |  |  |  |  |  |
| オランダ | 3-2 | - |  |  |  |  |
| アルゼンチン | 0-3 | 0-3 | - |  |  |  |
| ドイツ | 0-3 | 1-3 | 3-0 | - |  |  |
| カメルーン | 0-3 | 0-3 | 0-3 | 0-3 | - |  |
| メキシコ | 0-3 | 0-3 | 3-0 | 0-3 | 1-3 | - |
|  |  |  |  |  |  |  |
| POOL B | 中国 | イタリア | トルコ | ブルガリア | カナダ | キューバ |
| 中国 | - |  |  |  |  |  |
| イタリア | 3-1 | - |  |  |  |  |
| トルコ | 0-3 | 0-3 | - |  |  |  |
| ブルガリア | 1-3 | 0-3 | 0-3 | - |  |  |
| カナダ | 0-3 | 0-3 | 0-3 | 1-3 | - |  |
| キューバ | 0-3 | 0-3 | 1-3 | 0-3 | 1-3 | - |

これを、「国テーブル」と「対戦結果テーブル」に分割して登録してみました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8708/7c43bbdb-8417-b487-60c7-22067eae64e9.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/8708/04b4a26c-ae23-a5c1-9df4-d979f5bc4ee7.png)

## データベース作成の実装

まず、 `realmcreator/src/main/assets/` にCSVファイルを配置します。

そしてコードとしては、

- ActivityのonCreateで `Realm` オブジェクトを作成
- 古いデータを削除
- CSVデータを読みながら登録
- `writeCopyTo`を利用してサイズの縮小

といった流れです。

CSVの読み込みには、 [Apache Commons CSV](https://commons.apache.org/proper/commons-csv/)を利用しました。

```kotlin
package hm.orz.chaos114.android.realmcreator

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.text.TextUtils
import hm.orz.chaos114.android.realmcreator.entity.Country
import hm.orz.chaos114.android.realmcreator.entity.Match
import io.realm.Realm
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVRecord
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader


class MainActivity : AppCompatActivity() {

    lateinit var realm: Realm

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Realm.init(this)
        realm = Realm.getDefaultInstance()

        val outputFile = File(filesDir, "output.realm")
        deleteOldData(outputFile)

        createCountries()
        createMatch()

        // reduce size
        realm.writeCopyTo(outputFile)
    }

    private fun deleteOldData(outputFile: File) {
        realm.beginTransaction()
        realm.deleteAll()
        realm.commitTransaction()
        outputFile.delete()
    }

    private fun createCountries() {
        val reader = BufferedReader(InputStreamReader(resources.assets.open("match.csv")))
        reader.use {
            val records = CSVFormat.EXCEL.parse(reader)

            var poolName: String? = null
            realm.beginTransaction()
            records.records.forEach loop@{ record ->
                if (record.get(0).isEmpty()) {
                    poolName = null
                    return@loop
                }
                if (poolName == null) {
                    poolName = record.get(0)
                    return@loop
                }

                val obj = Country(
                        name = record.get(0),
                        pool = poolName!!
                )
                realm.copyToRealm(obj)
            }
            realm.commitTransaction()
        }
    }

    private fun createMatch() {
        val reader = BufferedReader(InputStreamReader(resources.assets.open("match.csv")))
        reader.use {
            val records = CSVFormat.EXCEL.parse(reader)

            var header: CSVRecord? = null
            realm.beginTransaction()
            records.records.forEach loop@{ record ->
                if (record.get(0).isEmpty()) {
                    header = null
                    return@loop
                }
                if (header == null) {
                    header = record
                    return@loop
                }

                val country1: Country = getCountry(record.get(0))
                record.forEachIndexed colLoop@{ index, colmn ->
                    if (index == 0) {
                        return@colLoop
                    }
                    if (TextUtils.isEmpty(colmn) || colmn.equals("-")) {
                        return@colLoop
                    }
                    val obj = Match(
                            country1 = country1,
                            country2 = getCountry(header!!.get(index)),
                            result = colmn
                    )
                    realm.copyToRealm(obj)
                }
            }
            realm.commitTransaction()
        }
    }

    private fun getCountry(name: String): Country {
        return realm.where(Country::class.java).equalTo("name", name).findFirst()!!
    }
}
```

`createCountries` と `createMatch` で同じようなループをしており、処理としては無駄が大きいのですが、それぞれのテーブル作成条件が変わったりといった仕様変更に耐えやすかと思い、冗長に書いています。

`Realm#writeCopyTo` については、これを行うことで出力されるファイルサイズが減るようです。
今回のサンプルでは、8kByteが4kByteといった差となりました。