【Firestore】「WhereIN」が使えない時の代案のまとめ

こんにちは。平気で N+1 を許容できるようになってきた Nash です。

この記事は、「Firestore のような NoSQL で WhereIN が使えないときの、代案をまとめた記事」です。

背景

数日前に下記の記事を書きました。

【Firestore】「orWhere」が使えない時の代案のまとめ

その中で、しれっと、「Firestore では、whereIN がないので〜」というふうに書いたのですが、ここらへんをまとめていなかったことに気付いたので、記事にしようと思った次第です。

Firestore で whereIN が使えるか?

使えないです。(2019/9 時点)

ただし、Node.js 側に提供されている SDK には getAll が提供されています。

ただ、これ、クエリレイヤーではなくて、SDK のレイヤーで forEach を回りしてるような気がしてます(TODO: きちんとコード確認していないので、あとで見る)

whereIN の 代案

というわけで、クエリレイヤー・SDK レイヤーで whereIN を実現できないので、アプリケーションレイヤーで解決しないといけないです。

案として下記2つ。

  • 【案 ①】すべてを一括で取得して、filter でデータをクレンジング
  • 【案 ②】N+1 クエリを投げて、結果を結合

実際に使うときは基本的には、案 ② になるかと思いますが、場合によっては案 ① のほうが良いケースもあります。

見ていきましょう。

ユースケース

下記のような、設計・ユースケースで考えます。

////// Entity

// firestore:users/{user}
type user = {
  id: string;
  // ...
};

// firestore:tweets/{tweet}
type tweet = {
  id: string;
  likedUserIDs: string[]; // いいね したUserIDのリスト
  // ...
};
  • Twitter みたいに、ツイートしたものに対して、任意の人が「いいね」出来る
  • ある Tweet の詳細画面で、「いいね」した User の一覧を取りたい。

【案 ①】すべてを一括で取得して、filter でデータをクレンジング

だいたいこんな感じになります。

// react-router から tweetIDを取る想定
const tweetID = match.params.tweetID;

// tweet を 取得
const snap = await db
  .collection('tweets')
  .doc(tweetID)
  .get();
const tweet = {id: snap.id, ...snap.data()};

// (A) まず、一括ですべて取得
const allFriends = await db.collection('users').get();

// (B) その後に、データクレンジング
const myFriends = allFriends.filter(maybeMyFriend =>
  tweet.likedFriendIDs.includes(maybeMyFriend.id)
);

return myFriends;
  • メリット:fetch 回数が少ない。
  • デメリット:filter の計算コストの処理時間がかかる
  • デメリット:無駄なデータまで fetch するので、帯域を余計に食う。
  • 特徴:大雑把に fetch するので、これらがセキュアな情報だとしたらセキュリティポリシー的に NG
  • 特徴:データ量が多いと変数格納のメモリ領域をかなり消費する。

結論、データ量が少なく、セキュアな情報でないときは、こちらがベターな選択、だと思っています。

そうでないなら、案 ② を採用する形になります。

【案 ②】N+1 クエリを投げて、結果を結合

だいたいこんな感じになります。

// react-router から tweetIDを取る想定
const tweetID = match.params.tweetID;

// (A) N+1の「1」のクエリ
const snap1 = await db
  .collection('tweets')
  .doc(tweetID)
  .get();
const tweet = {id: snap.id, ...snap.data()};

// (B) N+1の「N」のクエリ
const snap2List = tweet.friendIDs.map(friendID =>
  db.collection('users').doc(friendID).get()
);
const myFriends = snap2List.docs.map(doc => (
  { id: doc.id, ...doc.data() }
))

return myFriends;
  • メリット:必要なデータのみを fetch しているので、帯域を食わず、また、データのセキュリティポリシー的に NG にならない。
  • デメリット:fetch 回数が N+1 回必要。

所感

現状だと、たいていのケースでは案 ② の N+1 で実現してます。

Firestore を使っていると、非正規化だけでなくて、N+1 まで許容しないといけないので、RDS 脳が完全に崩壊します。(しました)

Nash
Nash

プログラミングが好きな人。SE→ITベンチャー→フリーランス。日本を出て、海外で働いて、最終ゴールは月で生活すること。