如何在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
相关产品推荐
相关产品推荐

