はじめに
アプリのアップデートを行う際に、新しいバージョンでは特定のドキュメントに新しいフィールドを追加したい場合があります。
例えば、古いバージョンのアプリで想定しているusersドキュメントが次のようなスキーマだったとします。
{
"name": string;
"createdAt": Timestamp;
}
そして、新しいバージョンのアプリでは、このドキュメントにaddressフィールドを追加したいと考えます。
{
"name": string;
"createdAt": Timestamp;
// 新しいフィールド
"address": {
postcode: string;
country: string;
state: string;
city: string;
address1: string;
address2: string;
}
}
このとき、新しいアプリでドキュメントを参照した時点では古いドキュメントのままで、addressフィールドは存在しないものとします。
この場合に、エラーを出さずにドキュメントを取得してaddressフィールドを参照する方法と更新するにはどうすればよいのでしょうか?
それぞれの方法について考えていきます。
エラーを出さずにaddressフィールドを参照する
usersドキュメントを表すUserモデルを次のように定義します。
data class User(
val id: String = "",
val name: String = "",
val createdAt: Timestamp = Timestamp(Date()),
)
そして、新しいバージョンのアプリでは、Userモデルを修正してaddressプロパティを追加します。このとき、addressフィールドの有無をわかりやすくするためにnullableにしておきます。
data class User(
val id: String = "",
val name: String = "",
val address: Address? = null,
val createdAt: Timestamp = Timestamp(Date()),
)
Addressは次のように定義します。
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を返すことができます。
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ドキュメントは次のスキーマとして認識していました。
{
"name": "Taro",
"createdAt": 2023-1-22 04:21:35 UTC+9
}
そして、新しいバージョンのアプリではusersドキュメントに新しくaddressフィールドを追加した状態でスキーマを認識して、これらの値に基づいて新しい機能を実装したいと考えています。
{
"name": "Taro",
"createdAt": 2023-1-22 04:21:35 UTC+9,
"address": {
"postcode": "060-0001",
"country" : "日本",
"state": "北海道",
"city": "札幌市",
"address1": "中央区北1条西2丁目1−7",
"address2": ""
}
}
実は、これについては問題なく通常通りupdate()を呼び出すことで解決することができます。
val data = hashMapOf<String, Any>(
"address" to hashMapOf<String, String>(
"postcode" to "060-0001",
"country" to "日本",
"state" to "北海道",
"city" to "札幌市",
"address1" to "中央区北1条西2丁目1−7",
"address2" to ""
)
)
val ref = firestore
.collection("users")
.document(userId)
ref.update(data).await()
update()を使うことで既存のフィールドに対して影響を与えることなく、スムーズに新しいフィールドに更新することができるようになります。
まとめ
RDBMSなどのデータベースのカラムに、新しいフィールドを追加したい時はデータの整合性を意識しながら慎重に作業を行う必要があります。
しかしFirestoreの場合には、その辺に関してはクライアント側がきちんとnullを許容できるクラスやロジックの設計を行うことで柔軟に新しいフィールドを追加することができるようになっています。
もちろんフィールド追加の濫用は、スキーマの整合性の観点からあまりお勧めされた行為ではないですが、課金機能を実装したいときにユーザーが有料プランのユーザーなのか無料プランのユーザーなのかのフラグをフィールドに追加したい場合などには簡単に対応することができます。