109
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Cloud FirestoreのSnapshot三種や他の型のまとめ

Last updated at Posted at 2021-01-11

Cloud Firestore のメソッドを使っていて、返ってきた値の扱い方について時々混乱することがありました。
Snapshot の種類と違いをもっと詳細に把握しておけば解決することにふと気づき、調べてみました。

類似記事

書き終わってから似た記事があることに気づきました。
まとめ方や重点の置き方に違いがあるので、それぞれに良さがあるのではないかと思います。

コード例などの言語

TypeScript と Dart/Flutter を使いながら調査しました。
記事中のコードは TypeScript にしていて、説明には Dart のことも含めています。
ウェブ SDK の言語は JavaScript と表現したりもしています。

他の言語で扱うときも大きくは違わないと思います。
クライアント用の SDK で確認したため、Admin SDK では異なるかもしれません。

関連する様々な型

Snapshot を細かく見る前に、よく使うプロパティの型やメソッド/ゲッターが返す値の型を全般的に見ておきましょう。

Referenceとget()

db.collection(...)  // CollectionReference
  .get()            // Promise<QuerySnapshot>

db.collection(...)  // CollectionReference
  .doc(...)         // DocumentReference
  .get()            // Promise<DocumentSnapshot>

CollectionReferenceDocumentReference は文字通り collection と document を指す参照なのでわかりやすいと思います。

そのどちらにも get() があり、基にするのが collection か document かで戻り値が異なります。
それは当然ですが、頭の中であまり区別せずに使ってしまうと混乱の原因になりそうです。

クエリ条件指定

db.collection(...)  // CollectionReference
  .where(...)       // Query
  .order(...)       // Query
  .limit(...)       // Query

db.collectionGroup(...)  // Query

先ほど collection() の戻り値は CollectionReference でしたね。
一方、collectionGroup()Query を返します。

Query はクエリ条件を指定するメソッド(where() など)が返すものです。
collectionGroup() も複数の同名の collection をまとめて取得する条件を指定するものであるせいか、where() 等に近い扱いになっているようです。1

複数のdocumentを取得

const snapshot = await db.collection(...).get()  // QuerySnapshot

snapshot
  .docs                     // QueryDocumentSnapshot[]
  .forEach(doc => { ... })  // QueryDocumentSnapshot

// JavaScript SDKでは直接forEach()できる
snapshot
  .forEach(doc => { ... })  // QueryDocumentSnapshot

CollectionReference を使って get() すると QuerySnapshot が得られます。
その中の docs は 配列になっています。
配列の中身の QueryDocumentSnapshot は後で詳しく見ます。

なお、JavaScript 用の SDK では QuerySnapshot のクラスに forEach() メソッドがあるので、docs の配列を取り出すことなく直接 forEach() で中身を順に取り出せます。
Dart/Flutter の SDK では直接はできません。

親を遡る

const doc = await db.doc('xxx/xxx').get()  // DocumentSnapshot

const ref1 = doc.ref         // DocumentReference
const ref2 = ref1.parent     // CollectionReference
const ref3 = ref2.parent     // DocumentReference(ないのでnull)
const d = await ref3?.get()  // DocumentSnapshot(ref3がnullなのでgetされずundefined)

QueryDocumentSnapshotDocumentSnapshotrefDocumentReference を得ることができます。
Dart では reference です。

parent を使って CollectionReference、更にその上の DocumentReference と遡っていけます。
ルート collection の参照で parent を使うと null が返ることに注意しましょう。

get() の結果は先ほどと違って QueryDocumentSnapshot ではなく DocumentSnapshot になります。

リアルタイムな監視

db.collection(...)
  .onSnapshot(snapshot => { ... })  // QuerySnapshot

db.collection(...)
  .doc(...)
  .onSnapshot(snapshot => { ... })  // DocumentSnapshot

onSnapshot(Dart では snapshots)でリアルタイムに更新を監視すると、コールバック関数に QuerySnapshotDocumentSnapshot が渡されます。

三種類のSnapshot

「Snapshot」が付くものが三つ出てきました。
私が特に混乱していたのはその三つです。

関連する様々な型 で既に概ね掴めたかもしれませんが、詳細を見ていきます。

※以下、プロパティが Dart ではゲッターになっている場合がありますが、特記は一部を除いて省略します。

QuerySnapshot

CollectionReferenceget() すると得られるものです。
クエリの結果として得られた QueryDocumentSnapshot のオブジェクトが docs という配列に入ります。

復習
db.collection(...)                  // CollectionReference
  .get()                            // Promise<QuerySnapshot>

db.collection(...)                  // CollectionReference
  .onSnapshot(snapshot => { ... })  // QuerySnapshot

DocumentSnapshot / QueryDocumentSnapshot とは異なり、get()data() はありません。
それら二つとは異質なので区別しやすいと思います。

主なプロパティ/メソッド

  • docs
    • クエリの結果として得られた document(QueryDocumentSnapshot)のオブジェクトの配列
      • document がなければ空の配列になります。
      • ここから document の情報を取り出して表示等に使うのが主な使い方になるかと思います。
  • size
    • クエリの結果として得られた document の数
  • metadata
    • SnapshotMetadata という型のデータ
      • バックエンドに保存できていないデータがあるか、キャッシュから取り出したデータか、といった情報を持っています。
  • docChanges
    • 前回から変化があった document の配列
      • onSnapshot(snapshot => { ... }) で変更を監視してリアルタイム更新するのに使えます。
      • 最初に取得したときには前回のデータがないので全ての document の情報が入っています。
    • Javascript ではメソッド、Dart ではゲッター

DocumentSnapshot

DocumentReferenceget() すると得られるものです。
document のデータを持っていて、data() で Map として取り出すことができます。2
データがない場合もあります。

復習
const doc = await db
  .doc('xxx/xxx/xxx/xxx')           // DocumentReference
  .get()                            // DocumentSnapshot

const ref = doc.ref.parent.parent   // DocumentReference
await ref?.get()                    // DocumentSnapshot

db.collection(...)                  // CollectionReference
  .doc(...)                         // DocumentReference
  .onSnapshot(snapshot => { ... })  // DocumentSnapshot

主なプロパティ/メソッド

  • id
    • document の ID です。
  • metadata
    • QuerySnapshotmetadata と同じ
  • exists
    • document の有無を表す bool 値
  • ref
    • document の参照
    • Dart では reference
  • data()
    • document のデータを返す
      • データがなければ JavaScript では undefined、Dart では null が返ります。
  • get()
    • document のフィールドの値を返す
      • フィールドの名前かパスを引数に取ります。

QueryDocumentSnapshot

CollectionReferenceget() して得た QuerySnapshotdocs 内にあるものです。

復習
const snapshot = await db.collection(...).get()  // QuerySnapshot

snapshot
  .docs                     // QueryDocumentSnapshot[]
  .forEach(doc => { ... })  // QueryDocumentSnapshot

DocumentSnapshot を継承していて API の見た目は同じです。

ただし、exists に違いが見られます。
「Query」という言葉が付いていることからイメージできるように、クエリの結果として得られたものであり、QuerySnapshotdocs に入っている時点で確実に存在するので、exists は常に true になります。

参考

API リファレンス

  1. 厳密に言うと where()limit() などが返すのは Query<T> で、collectionGroup() のほうは Query<DocumentData> です。ジェネリック型が DocumentData に固定されているかどうかの違いなので、それ以上深掘りしないでおきます。

  2. TypeScript では data() の戻り値は DocumentData という型ですが、これは文字列をキーとする Map のエイリアスです。

109
76
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
109
76

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?