Androidアプリ Realmでモデルパラメータを追加するとき、過去バージョン全てに渡ってパラメータ有無を考慮する必要があった話

SNSピリカのAndroidアプリについて、段階公開していたv5.4.13, v5.4.14リリースで起動クラッシュが発生する問題がありました。v5.4.13でRealmのバージョンアップおよびRealm操作周りの役割分離を行っており、そのなかでのマイグレーション処理の不備が原因でした。本稿ではその経緯を共有いたします。

<!-- more -->

## 背景
SNSピリカにはイベント機能があるのですが、イベント一覧をキャッシュしておくためにRealmライブラリを使っています。v5.4.12の時点で、[Realm](https://realm.io/)のバージョンはv7.0.8でした。そのため、Realmをv10.0.0以上にアップデートするためにアプリ側の修正対応が必要でした。Realmのv10.0.0から、データの書き込みはUIスレッド以外から行う必要があるためです。

また、当時Realmを操作する部分の処理がView操作と密結合だった問題があり、そのままだと保守が難しくなる懸念がありました。そこで、Realmのバージョンアップとともに、Realm操作周りとそれ以外で責務の分離を行いました。

## v5.4.12 → v5.4.13
### 修正内容
以下の図の通り修正しました。変更点は以下3つになります。

1. Realmを触るのは、Realm操作用のモジュールのみ(下図ではEventDatastore)にする
2. 永続化用データモデルは1.のモジュール内でのみ扱うようにし、外部ではビジネスロジック用のデータモデルを使うようにする
    - 例えば、イベント一覧の永続化用データモデルEventData(以後、旧EventDataと記載) を、 永続化用のEventDataPersist と ビジネスロジック用のEventDataに切り分け。1.内でのみEventDataPersistを使い、1.のインタフェース上ではEventDataを授受する
3. Realm v10.xへの対応: 書き込みトランザクションはUIスレッドではなく、Coroutineで発行したバックグラウンドスレッド上で実施する

![Items.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1967276/88f94058-9e0d-08be-caa2-cc7001b40932.jpeg)

### Realm v10以上での書き込み処理例

```diff
+ CoroutineScope(Dispatchers.Main).launch {
    realm.executeTransaction {
        for( event in eventDataList){
           realm.copyToRealmOrUpdate(event)
        }
    }
+ }
```

### 動作確認
アプリの動作が問題ないか確認するため、以下の観点で動作評価を行いました。

- 永続化したデータの書き込み・読み込みがv5.4.12と同等にできていること
- EventDataPersistが読み込まれ次第、Viewがアップデートされること

いずれの観点でも問題なかったため、v5.4.13のリリースに進みました。変更規模が数百行あることを踏まえ、段階公開20%から公開を始めました。

### 新たに発生した問題
公開から2日後、いくつかの端末にて起動後クラッシュしていることがわかりました。エラーの詳細を見てみると、

```
io.realm.exceptions.RealmMigrationNeededException
...
Migration is required due to the following errors: - Property 'EventPersistentModel.theParam' has been added.
```

とあり、theParamの定義が旧EventDataになかったことからクラッシュしていました。

Realmアップデート対応の元コードは、数ヶ月前に用意していました。しかしながら、そこからmergeするまでの間にEventDataにtheParamパラメータが別PRで追加されていました。当該PRをレビューの上、平行してEventData, EventPersistentModelにもパラメータ追加していたものの、マイグレーション処理で対応が漏れていました。

## v5.4.13 → v5.4.14
### 必要と考えていた修正内容
旧EventDataへのtheParamの追加はv5.4.10にて実施されていました。そこで、今回クラッシュが起きたのは、v5.4.9以下のアプリ、もしくはv5.4.10以上でもEventDataが未更新の場合と推測しました。実際に、v5.4.9→v5.4.10では問題なくアップデートできました。

そこで、マイグレーション処理で以下を追加しました。

```diff
+    realmSchema.get("EventData")
+        ?.addField(
+            "theParam",
+            Int::class.javaObjectType,
+            FieldAttribute.REQUIRED)
    realmSchema.rename(
        "EventData", "EventPersistentModel")
```

これにより、EventDataのスキーマにtheParamがない場合、theParamが追加されます。

### 動作確認
以下の観点で評価を行いました。いずれの点でも動作は問題ありませんでした。

- ✅ 項目1. v5.4.x-5.4.9以前のアプリで起動し、イベント一覧を開いてからv5.4.14にアップデートして起動できること
- ✅ 項目2. v5.4.x-5.4.9以前のアプリで起動し、イベント一覧を開いてから一度v5.4.13にアップデートする。起動直後クラッシュすることを確認してから、v5.4.14にアップデートして起動できること
- ✅ 項目3. v5.4.x-5.4.9以前のアプリで起動し、イベント一覧を開いてからv5.4.10-v.4.12にアップデートする。アプリ起動後、一度もイベント一覧を開かず、v5.4.14にアップデートして起動できること

上記動作確認がとれたため、そのままリリースに進みました。ただし、今回は念を入れて段階公開10%よりはじめました。

こちらで、Migrationの問題は解決したと思われましたが…

### 新たに発生した問題
v5.4.14にアップデートしたユーザで、すでにEventDataにtheParamがある旨で起動クラッシュが発生しました。前述の項目3.のパターンで過去のイベント一覧を見たときや、v5.4.10-5.4.12で起動の上でイベント一覧を見たときにはEventDataにtheParamが追加されます。そのため、今回のクラッシュが起こりました。

```
Fatal Exception: java.lang.IllegalArgumentException
Field already exists in 'EventData': theParam
```

## v5.4.14 → v5.4.15
### 対応内容
マイグレーション処理にて、まだtheParamがないときのみtheParamを追加するように修正しました。

```diff
-    realmSchema.get("EventData")
-    ?.addField(
+    val eventData = realmSchema.get("EventData")
+    if(eventData?.hasField("theParam") != true){
+        eventData?.addField(
             "theParam",
             Int::class.javaObjectType,
             FieldAttribute.REQUIRED)
+    }
     realmSchema.rename(
         "EventData", "EventPersistentModel")
```

### 動作確認
v5.4.14での動作確認に加え、一度v5.4.10-5.4.12にアップデートして、イベント一覧を過去数ヶ月分ロードした後に5.4.15にアップデートする観点を加えました。一連の動作確認で問題ないことを確認の上、v5.4.15を段階公開しました。v5.4.15では起動クラッシュは発生せず、問題なくアプリを利用できるようになりました。

## 振り返り: アップデートが2回上手くできなかった理由
いくつか原因はありますが、主なものは以下になります。

- Realmの変更内容がreviewされていない: 構成変更やBreaking Change対応があったので、reviewされるべきだった　→ Realm経験者が他に1名いるので、チェックする余地はあった
- Realmのマイグレーションの要否パターンが理解できていなかった: Realmのマイグレーションに対し、v5.4.10で問題なかったのでtheParamのマイグレーション対応が不要と考えていた。このため、v5.4.13での起動クラッシュにつながった。
  - v.5.4.12以前ではdeleteRealmIfMigrationNeededを使っており、マイグレーションが必要だが対応するマイグレーション処理がないときにRealm内のデータをクリアするようにしていた。そのため、今回の問題が発生していなかった。v5.4.13において、なるべくこれまでの永続化データを利用するために削除する形で対応したものの、その対応が今回の問題を発生させる遠因となった。
- 評価観点で、EventData.theParamのある/なしが網羅されていなかった: ここで網羅できていれば、v5.4.14でのリリース判定時に起動クラッシュの問題に気がつくことができていたはず

以上のことより、以下の対策が挙げられます。

- 今回の様に、PRの規模が大きいときはクロスチェックを入れる
  - OSのAPI変更対応、ライブラリのBreaking Change対応、仕様変更に伴う機能追加・変更は影響が大きいので必ず実施する
- 評価観点について、過去バージョンに対して永続化用データモデルのフィールドある/なしを網羅し、それら全てをチェックできる観点で評価項目を作成する

以上になります、ご覧いただきありがとうございます！