新機能開発時に役立つ！エラーなく新しいフィールドをFirestoreのドキュメントに追加する方法

# はじめに

アプリのアップデートを行う際に、新しいバージョンでは特定のドキュメントに新しいフィールドを追加したい場合があります。

例えば、古いバージョンのアプリで想定している`users`ドキュメントが次のようなスキーマだったとします。

```ts
{
    "name": string;
    "createdAt": Timestamp;
}
```

そして、新しいバージョンのアプリでは、このドキュメントに`address`フィールドを追加したいと考えます。

```ts
{
    "name": string;
    "createdAt": Timestamp;
    // 新しいフィールド
    "address": {
        postcode: string;
        country: string;
        state: string;
        city: string;
        address1: string;
        address2: string;
    }
}
```

このとき、新しいアプリでドキュメントを参照した時点では古いドキュメントのままで、`address`フィールドは存在しないものとします。

この場合に、エラーを出さずにドキュメントを取得して`address`フィールドを参照する方法と更新するにはどうすればよいのでしょうか？

それぞれの方法について考えていきます。

# エラーを出さずに`address`フィールドを参照する

`users`ドキュメントを表す`User`モデルを次のように定義します。

```kotlin
data class User(
    val id: String = "",
    val name: String = "",
    val createdAt: Timestamp = Timestamp(Date()),
)
```

そして、新しいバージョンのアプリでは、`User`モデルを修正して`address`プロパティを追加します。このとき、`address`フィールドの有無をわかりやすくするために`nullable`にしておきます。

```kotlin
data class User(
    val id: String = "",
    val name: String = "",
    val address: Address? = null,
    val createdAt: Timestamp = Timestamp(Date()),
)
```

`Address`は次のように定義します。

```kotlin
data class Address(
    val postcode: String = "",
    val country: String = "",
    val state: String = "",
    val city: String = "",
    val address1: String = "",
    val address2: String = "",
)
```

このとき、新しいアプリのバージョンで`toObject()`を使いながら`address`フィールドを参照するには次のようにします。

`?.let`を使うことで、「`address`フィールドが存在する場合」の処理を表しています。
そして、`address`フィールドが存在しない場合は`null`を返すことができます。

https://qiita.com/ngsw_taro/items/d29e3080d9fc8a38691e#%E4%B8%BB%E3%81%AA%E7%94%A8%E9%80%94

```kotlin
val snapshot = ref.get().await()
val snapshotData = snapshot.data
if (snapshotData == null) return null
val result = snapshot.toObject<User>()?.copy(
    id = snapshot.id,
    address = snapshotData["address"]?.let {
        val map = it as HashMap<String, String?>
        val postcode = map["postcode"] ?: ""
        val country = map["country"] ?: ""
        val state = map["state"] ?: ""
        val city = map["city"] ?: ""
        val address1 = map["address1"] ?: ""
        val address2 = map["address2"] ?: ""
        Address(
            postcode = postcode,
            country = country,
            state = state,
            city = city,
            address1 = address1,
            address2 = address2,
        )
    }
)
```

そして、もしなんらかの理由で`postcode`や`country`が不足した状態で`address`フィールドにマップが格納されていたとしても、エラーを出すことな`Address`インスタンスを格納することができます。

# エラーを出さずにまだ存在しないフィールドを更新する方法

それでは、新しいバージョンにアップデートされたばかりの状態で、`address`フィールドを持っていない`users`ドキュメントに対して新たに`address`フィールドを追加しつつ既存のデータの状態を保ったまま更新するにはどのようにすればよいのでしょうか？

それではもう一度おさらいしましょう。

古いバージョンのアプリでは、`users`ドキュメントは次のスキーマとして認識していました。

```js
{
    "name": "Taro",
    "createdAt": 2023-1-22 04:21:35 UTC+9
}
```

そして、新しいバージョンのアプリでは`users`ドキュメントに新しく`address`フィールドを追加した状態でスキーマを認識して、これらの値に基づいて新しい機能を実装したいと考えています。

```js
{
    "name": "Taro",
    "createdAt": 2023-1-22 04:21:35 UTC+9,
    "address": {
        "postcode": "060-0001",
        "country" : "日本",
        "state": "北海道",
        "city": "札幌市",
        "address1": "中央区北１条西２丁目１−７",
        "address2": ""
    }
}
```

実は、これについては問題なく通常通り`update()`を呼び出すことで解決することができます。

```kotlin
val data = hashMapOf<String, Any>(
    "address" to hashMapOf<String, String>(
        "postcode" to "060-0001",
        "country" to "日本",
        "state" to "北海道",
        "city" to "札幌市",
        "address1" to "中央区北１条西２丁目１−７",
        "address2" to ""
    )
)

val ref = firestore
    .collection("users")
    .document(userId)
ref.update(data).await()
```

`update()`を使うことで既存のフィールドに対して影響を与えることなく、スムーズに新しいフィールドに更新することができるようになります。

# まとめ

RDBMSなどのデータベースのカラムに、新しいフィールドを追加したい時はデータの整合性を意識しながら慎重に作業を行う必要があります。

しかしFirestoreの場合には、その辺に関してはクライアント側がきちんと`null`を許容できるクラスやロジックの設計を行うことで柔軟に新しいフィールドを追加することができるようになっています。

もちろんフィールド追加の濫用は、スキーマの整合性の観点からあまりお勧めされた行為ではないですが、課金機能を実装したいときにユーザーが有料プランのユーザーなのか無料プランのユーザーなのかのフラグをフィールドに追加したい場合などには簡単に対応することができます。