はじめに
アプリのアップデートを行う際に、新しいバージョンでは特定のドキュメントに新しいフィールドを追加したい場合があります。
例えば、古いバージョンのアプリで想定している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
を許容できるクラスやロジックの設計を行うことで柔軟に新しいフィールドを追加することができるようになっています。
もちろんフィールド追加の濫用は、スキーマの整合性の観点からあまりお勧めされた行為ではないですが、課金機能を実装したいときにユーザーが有料プランのユーザーなのか無料プランのユーザーなのかのフラグをフィールドに追加したい場合などには簡単に対応することができます。