💬
FirestoreにおけるN+1問題と向きあう(後編)
前編のおさらい
firestoreのqueryに対してページングしながら全件取得するAsync Generatorの実装例としてpaginate
を紹介しました。
export async function* paginate<T>(
query: Query<T>,
limit: number = 1000,
): AsyncGenerator<QueryDocumentSnapshot<T>, undefined, undefined> {
const { docs, size } = await query.limit(limit).get()
yield* docs
if (size === limit) yield* $paginate(query.startAfter(docs.at(-1)))
}
paginate
を使う側はfor await
でイテレートする
for await (const doc of paginate(users)) {
console.log(doc.id)
}
型が合わなくてyield*
を使えない問題があったが、 @ruiewo さんのおかげで解決できた!
戻り値となるAsyncGenerator
の型パラメータのNext
を指定せずデフォルトのunknown
になっていたのが問題だった。
for (const doc of docs) yield doc.data()
をyield* docs
に書き換えることができた。
firestoreでJOINを実現する
where in
で取得する
JOIN対象を-
firestore
のwhere in
は一度に取得できるデータの件数が30件までの制約がある-
N+1
をN/30+1
に削減するのが限界
-
- 疎(まばら)なJOIN対象と相性が悪い
- right側(JOINされる側)のデータが存在しない率が高いと無駄なクエリが増える
- 実装がちょっと厄介
- left側(JOINする側)のデータも30件ずつ取得するか、30件に分割する実装が必要
- その上でclient side join
paginateを応用してJOINを実現する!
innerJoin
する実装
documentIdで1対1対応しているcollection同士を- left側とright側それぞれdocumentId順でイテレートしていく
- left側とright側で対応するペアを見つけたらyeildする
export async function* $innerJoin<L, R>(
left: Query<L>,
right: Query<R>,
): AsyncGenerator<[QueryDocumentSnapshot<L>, QueryDocumentSnapshot<R>], undefined, undefined> {
// right側のpaginateはfor awaitでイテレートせず、イテレータープロトコルで扱う
const itr = $paginate(right.orderBy(FieldPath.documentId()))
// right側のイテレートが先行した場合にデータを保持しておく入れ物
const map: Map<string, QueryDocumentSnapshot<R>> = new Map()
// left側をイテレートする(念のためにorderByを指定)
for await (const leftDoc of $paginate(left.orderBy(FieldPath.documentId()))) {
// マッチするペアが見つかるまでループ
while (true) {
// next()でright側の次のdocを取得
const { done, value: rightDoc } = await itr.next()
// innerJoinなのでright側がdoneならbreak
if (done) break
else {
// マッチするペアが見つかったらyieldしてbreak
if (leftDoc.id === rightDoc.id) {
yield [leftDoc, rightDoc]
break
// mapからマッチするペアを探す
} else if (map.has(leftDoc.id)) {
const rightDoc = map.get(leftDoc.id)
map.delete(leftDoc.id)
yield [leftDoc, rightDoc!]
break
// マッチしない場合はrightDocをmapに保持
} else {
map.set(rightDoc.id, rightDoc)
// rightの方がイテレートが進んでいたらbreak
if (leftDoc.id < rightDoc.id) break
}
}
}
}
}
innerJoinを使う側の実装例
for await (const [left, right] of innerJoin(
collection("left"),
collection("right"),
)) {
console.log(left, right)
}
- かなり限定的な場面でしか使えない
- LEFT,RIGHT,FULL OUTER JOINも書けるはずだが手が回ってない
- documentId以外のキーでJOINするのも理屈的には可能だが、ユニーク制約がない点どうケアするかなど課題が多い
- innerJoinの実装はかなり煩雑で泥臭いけど、使う側のコードはすごくエレガントじゃない?
- まだ実験的な試みなので広めるには早い
Discussion