You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Firebase查询中使用查询游标实现分页(含字段过滤)

问题描述

我尝试为集合添加字段confirmed_booking_time的过滤条件,并使用该字段结合startAfter实现下一页分页。请问有人能指导如何在Firebase查询中使用查询游标实现分页吗?

现有代码

export const firestoreCollectionDocumentsJoin = (firestore: AngularFirestore, joins: Join[], clazz: any) => {
  return (source: Observable) => defer(() => {
    let documents: any[];
    const cache = new Map();

    return source.pipe(
      distinctUntilChanged((x, y) => {
        const x1 = x.filter((dca: DocumentChangeAction<unknown>) => {
          return !dca.payload.doc.metadata.hasPendingWrites;
        }).map((dca: DocumentChangeAction<unknown>) => {
          return set(dca.payload.doc.data() as any, 'id', dca.payload.doc.id);
        });

        const y1 = y.filter((dca: DocumentChangeAction<unknown>) => {
          return !dca.payload.doc.metadata.hasPendingWrites;
        }).map((dca: DocumentChangeAction<unknown>) => {
          return set(dca.payload.doc.data() as any, 'id', dca.payload.doc.id);
        });

        const c = x.map((dca: DocumentChangeAction<unknown>) => {
          return dca.payload.doc.metadata.fromCache;
        }).filter((d: any) => !!d);

        return false;
      }),
      switchMap((_dca: DocumentChangeAction<DocumentData>[]) => {
        cache.clear();
        const reads$ = [];
        let i = 0;

        documents = _dca.map(dca => {
          let doc = set(dca.payload.doc.data(), 'id', dca.payload.doc.id);
          if (!environment.production) {
            // console.log('doc ', doc);
          }
          doc = set(doc, 'metadata', dca.payload.doc.metadata);
          return doc;
        });

        for (const doc of documents) {
          for (const join of joins) {
            let docIds = get(doc, join.id);

            if (isString(docIds)) {
              docIds = [docIds];
            }

            if (isArray(docIds)) {
              for (const docId of docIds) {
                if (isString(docId)) {
                  if (cache.has(join.uniqueId(docId))) {
                    continue;
                  }

                  cache.set(join.uniqueId(docId), i);
                  i++;

                  reads$.push(
                    firestore
                      .collection(join.collection)
                      .doc(docId)
                      .snapshotChanges()
                      .pipe(
                        map(dca => {
                          return set(dca.payload.data() as any, 'id', dca.payload.id);
                        }),
                        distinctUntilChanged((p, c) => {
                          return JSON.stringify(p) === JSON.stringify(c);
                        }),
                      )
                  );
                }
              }
            }
          }
        }

        return reads$.length ? combineLatest(reads$) : of([]);
      }),
      map((values) => {
        return documents.map(document => {
          for (const join of joins) {
            const data = get(document, join.id);

            let docIds = [];
            if (isString(data)) {
              docIds = [data];
            } else if (isArray(data)) {
              docIds = data;
            }

            if (isArray(docIds)) {
              for (const docId of docIds) {
                if (isString(docId)) {
                  const uniqueId = join.uniqueId(docId);
                  const index = cache.get(uniqueId);
                  const docData = merge(values[index], set(values[index] as object, 'id', docId));

                  let currentDocData = get(document, join.data);
                  if (currentDocData) {
                    currentDocData = merge(currentDocData, new join.clazz(docData));
                    if (isArray(currentDocData)) {
                      document[join.data].push(new join.clazz(docData));
                    } else {
                      document = merge(document, set(document, join.data, currentDocData));
                    }
                  } else {
                    if (isArray(data)) {
                      document[join.data] = [new join.clazz(docData)];
                    } else if (isString(data)) {
                      document = merge(document, set(document, join.data, new join.clazz(docData)));
                    }
                  }
                }
              }
            }
          }
          if (!environment.production) {
            console.log('document ', new clazz(document));
          }

          return new clazz(document);
        });
      }),
      pipe(distinctUntilChanged((p, c) => {
        return JSON.stringify(p) === JSON.stringify(c);
      })),
      tap(docs => {
        if (!environment.production) {
          console.log(
            `Queried ${docs ? (docs as any).length : 0}, Joined ${cache.size} docs.`
          );
        }
      })
    );
  });
};
解决方案

Firestore的游标分页依赖排序字段和游标标记(通常是最后一条已加载文档的对应字段值),结合startAfter实现分页。以下是针对你的需求的具体实现步骤:

1. 构建带过滤和分页的基础查询

首先,构造包含confirmed_booking_time过滤、排序,以及startAfter游标参数的Firestore查询:

// 定义分页参数
const PAGE_SIZE = 10;
let lastVisible: DocumentSnapshot | null = null;

// 初始查询(第一页)
let query = firestore.collection('your-collection-name')
  .orderBy('confirmed_booking_time', 'desc') // 按预约时间降序排序
  .where('confirmed_booking_time', '>=', new Date('2024-01-01')) // 过滤条件示例
  .limit(PAGE_SIZE);

// 下一页查询:使用startAfter传入最后一条可见文档
if (lastVisible) {
  query = query.startAfter(lastVisible);
}

// 将查询转换为Observable,传入你的join操作符
query.snapshotChanges().pipe(
  firestoreCollectionDocumentsJoin(firestore, joins, YourClazz)
).subscribe(docs => {
  // 更新lastVisible为当前页最后一条文档的快照
  if (docs.length > 0) {
    // 从原始DocumentChangeAction数组中获取最后一条文档的快照
    lastVisible = _dca[_dca.length - 1].payload.doc;
  }
});

2. 调整现有join操作符以支持分页状态传递

为了更灵活处理分页,建议修改firestoreCollectionDocumentsJoin操作符,在输出中包含分页游标:

// 修改操作符返回类型,包含文档列表和分页信息
export const firestoreCollectionDocumentsJoin = (firestore: AngularFirestore, joins: Join[], clazz: any) => {
  return (source: Observable<DocumentChangeAction<DocumentData>[]>) => defer(() => {
    let documents: any[];
    let lastDocSnapshot: DocumentSnapshot | null = null;
    const cache = new Map();

    return source.pipe(
      distinctUntilChanged((x, y) => {
        // 保留原有逻辑
        return false;
      }),
      switchMap((_dca: DocumentChangeAction<DocumentData>[]) => {
        cache.clear();
        const reads$ = [];
        let i = 0;

        // 保存当前页最后一条文档的快照
        if (_dca.length > 0) {
          lastDocSnapshot = _dca[_dca.length - 1].payload.doc;
        }

        documents = _dca.map(dca => {
          // 保留原有文档转换逻辑
          let doc = set(dca.payload.doc.data(), 'id', dca.payload.doc.id);
          doc = set(doc, 'metadata', dca.payload.doc.metadata);
          return doc;
        });

        // 保留原有join关联逻辑
        for (const doc of documents) {
          for (const join of joins) {
            let docIds = get(doc, join.id);
            if (isString(docIds)) docIds = [docIds];
            
            if (isArray(docIds)) {
              for (const docId of docIds) {
                if (isString(docId) && !cache.has(join.uniqueId(docId))) {
                  cache.set(join.uniqueId(docId), i);
                  i++;
                  reads$.push(
                    firestore.collection(join.collection).doc(docId).snapshotChanges().pipe(
                      map(dca => set(dca.payload.data() as any, 'id', dca.payload.id)),
                      distinctUntilChanged((p, c) => JSON.stringify(p) === JSON.stringify(c))
                    )
                  );
                }
              }
            }
          }
        }

        return reads$.length ? combineLatest(reads$) : of([]);
      }),
      map((values) => {
        const resultDocs = documents.map(document => {
          // 保留原有文档关联转换逻辑
          for (const join of joins) {
            const data = get(document, join.id);
            let docIds = isString(data) ? [data] : (isArray(data) ? data : []);
            
            if (isArray(docIds)) {
              for (const docId of docIds) {
                if (isString(docId)) {
                  const uniqueId = join.uniqueId(docId);
                  const index = cache.get(uniqueId);
                  const docData = merge(values[index], set(values[index] as object, 'id', docId));
                  
                  let currentDocData = get(document, join.data);
                  if (currentDocData) {
                    currentDocData = merge(currentDocData, new join.clazz(docData));
                    document = isArray(currentDocData) 
                      ? {...document, [join.data]: [...document[join.data], new join.clazz(docData)]}
                      : merge(document, set(document, join.data, currentDocData));
                  } else {
                    document = isArray(data)
                      ? {...document, [join.data]: [new join.clazz(docData)]}
                      : merge(document, set(document, join.data, new join.clazz(docData)));
                  }
                }
              }
            }
          }
          return new clazz(document);
        });

        // 返回文档列表和分页游标
        return {
          docs: resultDocs,
          lastVisible: lastDocSnapshot
        };
      }),
      distinctUntilChanged((p, c) => JSON.stringify(p.docs) === JSON.stringify(c.docs)),
      tap(result => {
        if (!environment.production) {
          console.log(`Queried ${result.docs.length}, Joined ${cache.size} docs.`);
        }
      })
    );
  });
};

3. 组件中实现分页触发逻辑

在组件内通过按钮点击等事件触发下一页加载:

// 组件中的分页方法
loadNextPage() {
  let query = firestore.collection('your-collection-name')
    .orderBy('confirmed_booking_time', 'desc')
    .where('confirmed_booking_time', '>=', new Date('2024-01-01'))
    .limit(PAGE_SIZE);

  if (this.lastVisible) {
    query = query.startAfter(this.lastVisible);
  }

  query.snapshotChanges().pipe(
    firestoreCollectionDocumentsJoin(firestore, joins, YourClazz)
  ).subscribe(result => {
    // 合并新页数据到现有列表(或替换,根据需求)
    this.docs = [...this.docs, ...result.docs];
    this.lastVisible = result.lastVisible;
  });
}

关键注意事项

  • 必须先排序:startAfter依赖查询的排序字段,必须先调用orderBy指定confirmed_booking_time,才能使用该字段作为游标。
  • 复合游标:若存在多个文档的confirmed_booking_time相同,建议使用复合排序(如orderBy('confirmed_booking_time').orderBy('id')),避免分页时重复或遗漏文档。
  • 实时更新处理:如果使用snapshotChanges()监听实时更新,可能会打乱分页状态,可考虑分页时切换到get()获取单次数据,或额外处理实时更新的分页兼容逻辑。

内容的提问来源于stack exchange,提问作者Sarbjit Singh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.18 04:52:01