返回

Firestore 多查询结果聚合与实时更新解决方案

javascript

Firestore 多查询结果聚合与实时更新

在 Firestore 应用开发中,有时需要将多个查询的结果合并成一个数据集,并且保持对数据库的实时更新同步。当查询条件受限于 Firestore 的特定规则(例如 in 查询的文档数量限制),或者需要合并来自不同查询的数据时,这个问题就会变得更加复杂。本文将探讨如何解决这个问题,并提供可行的解决方案。

问题分析

当单个 Firestore 查询无法满足需求时,需要将多个查询组合起来。Firestore 的 in 查询有 30 个文档的限制,当需要根据 ID 查询大量文档时,必须将查询分批进行。而 vector search 功能依赖 Firebase Function,返回的数据也需要通过 ID 重新查询 Firestore 文档,这进一步加剧了问题复杂性。同时,保持与 Firestore 的实时数据同步也至关重要。我们需要找到一种方法,既能处理多个查询,又能将结果合并,并且能够响应数据库的实时更新。

解决方案

1. 客户端手动合并查询结果并监听

此方案通过在客户端手动处理多个查询,并将结果合并到一个响应式数组中。该方案利用 onSnapshot 监听每个查询的数据变化,并通过计算文档在数组中的偏移量来更新数组,以保证数据的实时性和一致性。

原理:

  • 将 ID 数组分批,每批不超过 Firestore 的 in 查询限制(通常为 30)。
  • 为每批 ID 创建一个 Firestore 查询。
  • 使用 onSnapshot 监听每个查询的数据变化。
  • onSnapshot 的回调函数中,根据文档变化类型(添加、修改、删除)更新聚合数组。添加时,计算文档的偏移量并插入数组;修改时,先移除旧文档再插入新文档;删除时,直接移除文档。

步骤:

  1. 将 ID 数组分批:

    function slices(array: any[], size: number) {
      const result = [];
      for (let i = 0; i < array.length; i += size) {
        result.push(array.slice(i, i + size));
      }
      return result;
    }
    
  2. 创建多个 Firestore 查询:

    import { db } from './firebase'
    import { collection, query, where, documentId, Query, QuerySnapshot, DocumentData } from 'firebase/firestore'
    
    const FIRESTORE_ID_QUERY_LIMIT = 30
    
    export function queryDocumentsByIds(collectionPath: string, ids: string[]): Query[] {
      const queries: Query[] = []
      const batches = slices(ids, FIRESTORE_ID_QUERY_LIMIT)
      for (const batch of batches) {
        const q = query(collection(db, collectionPath), where(documentId(), 'in', batch))
        queries.push(q)
      }
      return queries
    }
    
  3. 监听多个查询并合并结果:

    import { onSnapshot } from 'firebase/firestore'
    import { ref, Ref } from 'vue'
    
    function onErrorHandler(error:any){
      console.error("An error occurred with the query subscription: ", error);
    }
    
    export function useQueryArray(collectionPath: string, ids: string[]): Ref<any[]> {
      const multiRef = ref([]) as Ref<any[]>
      const queries: Query[] = queryDocumentsByIds(collectionPath, ids)
    
      for (let i = 0; i < queries.length; i++) {
        const q = queries[i]
        const offset = i * FIRESTORE_ID_QUERY_LIMIT
    
        onSnapshot(q, (snapshot: QuerySnapshot<DocumentData, DocumentData>) => {
          snapshot.docChanges().forEach(change => {
            const { oldIndex, newIndex, doc, type } = change
            if(newIndex == -1) return;
            const actualIndex = offset + newIndex
    
            if (type === 'added') {
              multiRef.value.splice(actualIndex, 0, {id: doc.id, ...doc.data()})
            } else if (type === 'modified') {
                const indexToRemove =  oldIndex === -1 ? actualIndex : offset + oldIndex
              multiRef.value.splice(indexToRemove, 1)
              multiRef.value.splice(actualIndex, 0, {id: doc.id, ...doc.data()})
            } else if (type === 'removed') {
                const indexToRemove =  oldIndex === -1 ? actualIndex : offset + oldIndex
              multiRef.value.splice(indexToRemove, 1)
            }
          })
        },  onErrorHandler)
      }
      return multiRef
    }
    

示例代码:
```vue