TL;DR
Firebase (Firestore)でデータを管理していて、DocumentReference.update(data)
を使用してデータを更新するようなシステムを開発していたら、update
は成功しているのにfirestore上で値が書き換わらないということが起った。どうやら与えるマップのフィールド名 "key.hoge"
は key: { hoge: value}
というように違うレベルのフィールドとして認識されてしまうらしい。
そのため、下記を気を付ける。
- データ更新の際にもDocumentReference.set() を使い
SetOptions(merge: true)
付ける。(mergeを絶対つけること。でないと他のフィールドが消える) - (そもそもドット付きのフィールド(
key.hoge
)なんて使わない)
不具合・問題・症状
症状として、個人開発中のスマホアプリでユーザが持つあるクラスのオブジェクトをFirebaseのあるコレクション上で保存していて、既存のデータが更新できない、という状態に陥った。
コレクション上のDocumentReference
から docRef.update(data)
を行い既存のデータを更新しようとして、update
メソッド自体は正常にdocRef返してonError
などでキャッチされていないため、コレクション上には無事に書き込まれているはずなのに、firebase上 (console.firebase.google.com
)で該当ドキュメントを確認しても更新されていない。
現状の状態と手続きとしては、
- 新規保存 (
Class.toJson
→docRef.set()
で保存) - フェッチしてオブジェクトを復元が可能な状態 (
docRef.data()
やsnapshot.data()
でフェッチ後→Class.fromJson
で復元) - 更新は成功するが(
docRef.update(object.toJson())
が問題なく終了)、Cloud Firestoreでは更新されていない(再度フェッチしてfromJson
で復元しても値が変わっていない)。
という状態。
- ドキュメントの構造
- フィールドの形式は
Map<String, dynamic>
でマップが入れ子になっているような状態。 - フィールド名などは
"hogeClass.Id"
:"hoge"
"hogeClass.childClass"
:"childClass.id"
:"hoge"
"childClass.field"
:"hoge"
"hogeClass.field"
:"hoge"
という具合
- フィールドの形式は
そこで、Cloud Firestoreで更新されるはずだったドキュメントを改めて確認してみると、
"hogeClass.Id"
:"hoge"
"hogeClass.childClass"
:"childClass.id"
:"hoge"
"childClass.field"
:"hoge"
"hogeClass.field"
:"hoge"
に加えて、
"hogeClass"
:"Id"
:"hoge"
"childClass"
:"id"
:"hoge"
"field"
:"更新済みの値"
"field"
:"更新済みの値"
という新しいネストされたフィールドが出来上がっていて、さらに値も更新されている、という状態であることが発覚。
つまり、{"hogeClass.Id":"hoge", …}
とMap形式で渡したものが{"hogeClass": {"Id": "hoge"}, …}
というようにMap<String, Map<String,dynamic>>
のようにドット前後で分解されて"key.nestedKey": "nestedFieldValue"
というようにネストされたデータだと解釈・保存されてしまっている。なぜ…🤔
原因
Firebaseのデータ更新のための DocumentReference.update()
は
docRef.update({"key.hoge": "value"}, )
とするとドットを含んでいるキー "key.hoge"
が階層に分けられて {"key": {"hoge": "value"}}
として保存されてしまう。
逆に新規追加・上書きによく用いられるDocumentReference.set()
の場合、docRef.set({"key.hoge": "value"}, )
とすると階層として展開はされず、"key.hoge"
にそのまま"value"
が格納される。
そのため、新規追加のときにdocRef.set("key.hoge":"value")
を使用して、更新でも同じようにdocRef.update("key.hoge":"value2")
とすると更新しているように見えて全く違う値が書き換えられる(更新されていないように見えてしまう)。
どうやらそれが仕様らしく(?)、そういう書きかたがされている様子(詳細は最後のおまけに記載)。
解決策
もはや "key.hoge"
というフィールドでFirestoreのデータ構造のデータベースが出来上がって使用されていた場合、"key.hoge"
を "key": {"hoge": ...}
に置き換えるなんて作業はしたくない(オブジェクトを復元するときのfromJson()
とかも書き換えなきゃだし絶対に嫌!)。
key.hoge
というようにドットを含んだキーのフィールドを更新したい場合は、調べたところupdate
ではなくset
にSetOptions
をつけてdocRef.set(map, SetOptions(merge: true))
とすると、マージ(更新)できるらしい。
(逆にsetでmergeをつけないと上書きされて今うので要注意 ← 与えたmap中にないフィールドは消されてしまう)
まとめ(所感)
- もしくはデータ更新の際にもDocumentReference.set() を使い
SetOptions(merge: true)
付ける。 DocumentReference.set(data)
としてmergeをつけないと、他のフィールドが消えるので、実装するときは注意すること(上書きされていまう)。- そもそもドット付きのフィールド(
key.hoge
)なんて使わない方がいい。- それはそれとして、setとupdateで仕様が異なり、setで新規追加時に「こう保存されるのか」と確認したものが updateに同じような形式でデータを与えると異なる解釈がされて全く違う結果となる、という罠がある。初学者(自分)は注意が必要。
おまけ
DocumentReference.set
とDocumentReference.update
では別のデータ処理が行われている様子。
set
では_CodecUtility.replaceValueWithDelegatesInMap
、update
では_CodecUtility.replaceValueWithDelegatesInMapFieldPath
にデータが渡されて、フィールドが階層構造に(updateでは)分けられたり、(setでは)分けられなかったりする。
@override Future<void> set(Map<String, dynamic> data, [SetOptions? options]) { return _delegate.set( _CodecUtility.replaceValueWithDelegatesInMap(data)!, options, ); } @override Future<void> update(Map<Object, Object?> data) { return _delegate .update(_CodecUtility.replaceValueWithDelegatesInMapFieldPath(data)!); }