🔥

【Firebase】Cloud Firestore クエリ まとめ

2020/11/08に公開

ウェブ版 CloudFirestoreのクエリ備忘録
日本語版公式ドキュメントに載っていないものがいくつかあったので、まとめてみた。
※コレクショングループについては書いていません。

更新情報

2023/5/13

  • ORクエリについて追記しました

2022/2/10

前提となるデータ構造

Firestoreの中身は以下を想定

Firestoreのデータ構造
// コレクション
users: {
  // ドキュメント
  "000": {
    // フィールド
    address: {
      prefecture: "沖縄",
      region: "沖縄",
    },
    age: 20,
    createdAt: 2020111015:00:00 UTC+9,
    friends: ["003"],
    userName: "山田太郎",
  },
  "001": {
    address: {
      prefecture: "北海道",
      region: "北海道",
    },
    age: 20,
    createdAt: 202011812:00:00 UTC+9,
    friends: [],
    userName: "小島三郎",
  },
  "002": {
    address: {
      prefecture: "東京",
      region: "関東",
    },
    age: 19,
    createdAt: 20201119:14:00 UTC+9,
    friends: ["001"],
    userName: "山田花子",
  },
}

where:クエリ演算子

利用可能なクエリ一覧

演算子 説明 書き方
< ~より小さい .where("age", "<", 25)
<= ~以下 .where("age", "<=", 25)
== ~と等しい .where("userName", "==", "山田太郎")
!= ~と等しくない .where("userName", "!=", "山田太郎")
> ~より大きい .where("age", ">", 20)
>= ~以上 .where("age", ">=", 20)
array-contains 配列内に、右辺の要素が含まれている .where("friends", "array-contains", "003")
array-contains-any 配列内に、右辺の要素のいずれかが含まれている .where("friends", "array-contains-any", ["002", "003"])
in 右辺のいずれかが含まれている .where("friends", "in", ["001", "002"])
not-in 右辺のいずれも含まれない .where("friends", "not-in", ["001", "002"])

<:~より小さい

未成年(20歳未満)のユーザを抽出

(v8)未成年のユーザを取得.js
const db = firebase.firestore();

db.collection("users").where("age", "<", 20).get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)未成年のユーザを取得.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("age", "<", 20))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
002: 山田花子

<=:~以下

ユーザIDが001以下のユーザを抽出

firebase.firestore.FieldPath.documentId()でドキュメントIDを参照できる

(v8)ユーザIDが001以下のユーザを取得.js
const db = firebase.firestore();

db.collection("users").where(firebase.firestore.FieldPath.documentId(), "<=", "001").get().then(snapshot => {
    snapshot.forEach(doc => {
        console.log(`${doc.id}: ${doc.data().userName}`);
    })
})
(v9)ユーザIDが001以下のユーザを取得.js
import {
  collection,
  documentId,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where(documentId(), "<=", "001"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎
001: 小島三郎

==:~と等しい

東京在住のユーザを抽出

フィールド内のオブジェクトにあるプロパティはjsで参照するときと同様に、オブジェクト名.プロパティ名で参照できる

(v8)東京在住のユーザを抽出.js
const db = firebase.firestore();

db.collection("users").where("address.prefecture", "==", "東京").get().then(snapshot => {
    snapshot.forEach(doc => {
        console.log(`${doc.id}: ${doc.data().userName}`);
    })
})
(v9)東京在住のユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("address.prefecture", "==", "東京"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
002: 山田花子

!=:~と等しくない

東京に住んでいないユーザを抽出

東京に住んでいないユーザを抽出.js
const db = firebase.firestore();

db.collection("users").where("address.prefecture", "!=", "東京").get().then(snapshot => {
    snapshot.forEach(doc => {
        console.log(`${doc.id}: ${doc.data().userName}`);
    })
})
(v9)東京に住んでいないユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("address.prefecture", "!=", "東京"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎
001: 小島三郎

>=:~以上

作成日が2020年11月9日以降のユーザを抽出

FirebaseのTimestamp型は、日付オブジェクトをクエリに利用できる

作成日が2020年11月9日以前のユーザを抽出.js
// 2020年11月9日の日付インスタンスを生成
const targetDate = new Date("2020-11-09");

const db = firebase.firestore();
db.collection("users").where("createdAt", ">=", targetDate).get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)作成日が2020年11月9日以前のユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

// 2020年11月9日の日付インスタンスを生成
const targetDate = new Date("2020-11-09");

getDocs(query(usersRef, where("createdAt", ">=", targetDate))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎

array-contains:配列内に、右辺の要素が含まれている

ユーザID「003」がフレンドのユーザを抽出

(v8)ユーザID「003」がフレンドのユーザを抽出.js
const db = firebase.firestore();
db.collection("users").where("friends", "array-contains", "003").get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)ユーザID「003」がフレンドのユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("friends", "array-contains", "003"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎

array-contains-any:配列内に、右辺の要素のいずれかが含まれている

ユーザID「002, 003, 004」のいずれかがフレンドのユーザを抽出

(v8)ユーザID「003」がフレンドのユーザを抽出.js
const db = firebase.firestore();

db.collection("users").where("friends", "array-contains-any", ["001", "002", "003"]).get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)ユーザID「003」がフレンドのユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("friends", "array-contains-any", ["001", "002", "003"]))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎

in:右辺のいずれかが含まれている

ユーザ名が「山田太郎」もしくは「山田花子」を抽出

(v8)ユーザ名が「山田太郎」もしくは「山田花子」を抽出.js
const db = firebase.firestore();

db.collection("users").where("userName", "in", ["山田太郎", "山田花子"]).get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)ユーザ名が「山田太郎」もしくは「山田花子」を抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("userName", "in", ["山田太郎", "山田花子"]))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎
002: 山田花子

not-in:右辺のいずれも含まれない

ユーザ名が「山田太郎」でも「山田花子」でもないユーザを抽出

(v8)ユーザ名が「山田太郎」でも「山田花子」でもないユーザを抽出.js
const db = firebase.firestore();

db.collection("users").where("userName", "not-in", ["山田太郎", "山田花子"]).get().then(snapShot => {
  snapShot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)ユーザ名が「山田太郎」でも「山田花子」でもないユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("userName", "not-in", ["山田太郎", "山田花子"]))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
001: 小島三郎

複合クエリ(And検索)

.where().where()のようにつなげることでAnd検索が可能

(v8)20代のユーザを抽出.js
const db = firebase.firestore();

db.collection("users").where("age", ">=", 20).where("age", "<=", 29).get().then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})
(v9)20代のユーザを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, where("age", ">=", 20), where("age", "<=", 29))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎
001: 小島三郎

ORクエリ

or(where(), where())のようにすることでOR検索が可能

(v9)userNameが山田太郎もしくはageが19のデータを抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  query,
  where,
  or
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, or(where("userName", "==", "山田太郎"), where("age", "==", 19)))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
000: 山田太郎
002: 山田花子

orderBy:並び替え

ソートはorderByで行う。orderBy("age")とすることで、ageで昇順ソート、orderBy("age", "desc")とすると、ageで降順ソートされる。

年齢を昇順で表示

(v8)年齢で昇順ソート.js
const db = firebase.firestore();

db.collection("users").orderBy("age").get().then(snapshot => {
  snapshot.forEach(doc => {
    const data = doc.data()
    console.log(`${data.userName}, ${data.age}`);
  })
})
(v9)年齢で昇順ソート.js
import {
  collection,
  getDocs,
  getFirestore,
  orderBy,
  query,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, orderBy("age"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
山田花子, 19歳
山田太郎, 20歳
小島三郎, 29

年齢を降順で表示

(v8)年齢で降順ソート.js
const db = firebase.firestore();

db.collection("users").orderBy("age", "desc").get().then(snapshot => {
  snapshot.forEach(doc => {
    const data = doc.data()
    console.log(`${data.userName}, ${data.age}`);
  })
})
(v9)年齢で降順ソート.js
import {
  collection,
  getDocs,
  getFirestore,
  orderBy,
  query,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, orderBy("age", "desc"))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
小島三郎, 29歳
山田太郎, 20歳
山田花子, 19

複数条件でソート

orderBy().orderBy()とすることで、1つ目のordeBy()の項目が等しかったら、2つ目のorderBy()の項目で並び替えが行われる

limit:最大表示件数

最大表示件数の指定はlimit()で行う。limit(1)とすると最大1件分、limit(10)とすると最大10件分取得する

(v8)1件分抽出.js
const db = firebase.firestore();

db.collection("users").limit(1).get().then(snapshot => {
  snapshot.forEach(doc => {
    const data = doc.data()
    console.log(`${data.userName}, ${data.age}`);
  })
})
(v9)1件分抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  limit,
  query,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getDocs(query(usersRef, limit(1))).then(snapshot => {
  snapshot.forEach(doc => {
    console.log(`${doc.id}: ${doc.data().userName}`);
  })
})

上記の結果は以下の通り。

抽出結果
山田太郎, 20

count:集計クエリ

Firestoreのドキュメントの数を集計するにはgetCountFromServerを利用する

(v9)1件分抽出.js
import {
  collection,
  getDocs,
  getFirestore,
  getCountFromServer,
} from 'firebase/firestore';

const db = getFirestore();
const usersRef = collection(db, "users");

getCountFromServer(usersRef).then(snapshot => {
  console.log(snapshot.data().count)
})

上記の結果は以下の通り。

抽出結果
3

できないことまとめ

コレクションやドキュメントに対するワイルドカード

以下のようなワイルドカードは不可

NG.js
const db = firebase.firestore();
db.collection("users").document("*").where( /* 以下省略 */

フィールド内のオブジェクトのキーに対するクエリ

今回のDB構造でいう、addressに対してのクエリはできない
例えば、オブジェクトのキーをエポック秒(UNIX時間)にしたとして、一定の時刻以下のマップを持つユーザ、のようなクエリはできない

NG.js
// DB構造
users: {
  "004": {
    "1577804400": {
      "hoge": "fuga",
    }
  }
}
// 上記の1577804400に対するクエリはできない

Discussion