【问题标题】:Google Firestore - How to get several documents by multiple ids in one round-trip?Google Firestore - 如何在一次往返中通过多个 id 获取多个文档?
【发布时间】:2018-06-16 08:46:51
【问题描述】:

我想知道是否可以在到 Firestore 数据库的一次往返(网络调用)中通过 id 列表获取多个文档。

【问题讨论】:

  • 您似乎认为往返会导致您的应用出现性能问题。我不会这么认为。 Firebase 有在这种情况下表现良好的历史,因为它pipelines the requests。虽然我还没有检查 Firestore 在这种情况下的行为方式,但我希望在假设存在性能问题之前先看到性能问题的证据。
  • 假设我需要文件abc 来做某事。我在单独的请求中并行请求所有三个。 a 需要 100 毫秒,b 需要 150 毫秒,c 需要 3000 毫秒。结果,我需要等待 3000 毫秒才能完成任务。其中将是max。当要获取的文档数量很大时,风险会更大。取决于网络状态,我认为这可能会成为一个问题。
  • 虽然将它们全部发送为单个SELECT * FROM docs WHERE id IN (a,b,c) 不会花费相同的时间吗?我看不出有什么区别,因为连接只建立一次,其余的都是通过管道传输的。时间(在初始建立连接之后)是所有文档的加载时间 + 1 次往返,这两种方法都相同。如果它对您来说表现不同,您能否分享一个示例(如我的链接问题中所示)?
  • 我想我失去了你。当您说它是流水线时,您的意思是 Firestore 自动分组并将查询发送到他们的服务器一次往返数据库?
  • 是的,我已经阅读了您的回答,但目前还不清楚是否会有多个网络调用或只有一个。听起来n 项目将同时进行n 网络调用,而不仅仅是一个同时执行n 查询的网络调用。

标签: javascript node.js firebase google-cloud-platform google-cloud-firestore


【解决方案1】:

如果你在 Node 中:

https://github.com/googleapis/nodejs-firestore/blob/master/dev/src/index.ts#L978

/**
* Retrieves multiple documents from Firestore.
*
* @param {...DocumentReference} documents - The document references
* to receive.
* @returns {Promise<Array.<DocumentSnapshot>>} A Promise that
* contains an array with the resulting document snapshots.
*
* @example
* let documentRef1 = firestore.doc('col/doc1');
* let documentRef2 = firestore.doc('col/doc2');
*
* firestore.getAll(documentRef1, documentRef2).then(docs => {
*   console.log(`First document: ${JSON.stringify(docs[0])}`);
*   console.log(`Second document: ${JSON.stringify(docs[1])}`);
* });
*/

这是专门为服务器 SDK 设计的

更新: “Cloud Firestore [client-side sdk] 现在支持 IN 查询!”

https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html

myCollection.where(firestore.FieldPath.documentId(), 'in', ["123","456","789"])

【讨论】:

  • 对于任何希望使用动态生成的文档引用数组调用此方法的人,您可以这样做:firestore.getAll(...arrayOfReferences).then()
  • 我很抱歉@KamanaKisinga ...我已经将近一年没有做过任何 Firebase 的东西了,现在也帮不上什么忙(嘿看,我实际上在一年前发布了这个答案今天!)
  • 客户端 SDK 现在也提供此功能。例如,请参阅 jeodonara 的答案:stackoverflow.com/a/58780369
  • 警告:in 过滤器目前限制为 10 个项目。因此,当您即将投入生产时,您可能会发现它毫无用处。
  • 其实你需要使用firebase.firestore.FieldPath.documentId()而不是'id'
【解决方案2】:

他们刚刚宣布了这个功能,https://firebase.googleblog.com/2019/11/cloud-firestore-now-supports-in-queries.html

现在您可以使用类似的查询,但请注意输入大小不能大于 10。

userCollection.where('uid', 'in', ["1231","222","2131"])

【讨论】:

  • 有一个 whereIn 查询而不是 where。而且我不知道如何从属于特定集合的文档 ID 列表中设计对多个文档的查询。请帮忙。
  • @Compileerrorend 你能试试这个吗? db.collection('users').where(firebase.firestore.FieldPath.documentId(), 'in',["123","345","111"]).get()
  • 谢谢你,特别是firebase.firestore.FieldPath.documentId()
  • @jeadonara 如果输入数组大于 10,我应该使用什么?
  • @RameshVishnoi 你可以使用 Promise.all() (developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…)。
【解决方案3】:

实际上你会像这样使用 firestore.getAll

async getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    const users = await this.firestore.getAll(...refs)
    console.log(users.map(doc => doc.data()))
}

或使用承诺语法

getUsers({userIds}) {
    const refs = userIds.map(id => this.firestore.doc(`users/${id}`))
    this.firestore.getAll(...refs).then(users => console.log(users.map(doc => doc.data())))
}

【讨论】:

  • 这确实应该是选择的答案,因为它可以让您使用超过 10 个 ids
  • 这确实有效!谢谢。这方面的文档在哪里?我找了 getAll,但在任何地方都找不到。
  • @TravRob 这可能在某些版本的 Firebase 中可用,例如 Node,但它绝对不在 JavaScript API 中。
【解决方案4】:

你可以使用这样的函数:

function getById (path, ids) {
  return firestore.getAll(
    [].concat(ids).map(id => firestore.doc(`${path}/${id}`))
  )
}

可以用一个ID调用:

getById('collection', 'some_id')

或一组 ID:

getById('collection', ['some_id', 'some_other_id'])

【讨论】:

    【解决方案5】:

    不,目前无法使用 Cloud Firestore SDK 批处理多个读取请求,因此无法保证您可以一次读取所有数据。

    但是,正如 Frank van Puffelen 在上面的 cmets 中所说,这并不意味着获取 3 个文档的速度将是获取一个文档的 3 倍。在这里得出结论之前最好自己进行测量。

    【讨论】:

    • 问题是我想在迁移到 Firestore 之前了解 Firestore 性能的理论限制。我不想迁移然后意识到它对我的用例来说不够好。
    • 你好,这里也有考量cos。假设我存储了我所有朋友的 ID 列表,数量为 500。我可以在 1 次读取成本中获得列表,但为了显示他们的姓名和照片 URL,我需要 500 次读取。
    • 如果您尝试读取 500 个文档,则需要读取 500 次。如果您将所有 500 个文档中所需的信息合并到一个额外的文档中,则只需阅读一次。在包括 Cloud Firestore 在内的大多数 NoSQL 数据库中,这种所谓的数据重复是很正常的。
    • @FrankvanPuffelen 例如,在 mongoDb 中,您可以像这样使用 ObjectId stackoverflow.com/a/32264630/648851
    • 就像@FrankvanPuffelen 所说,数据重复在 NoSQL 数据库中很常见。在这里,您必须问自己需要多久读取一次这些数据,以及它们需要保持多长时间的最新状态。如果您确实存储了 500 个用户信息,比如说他们的姓名+照片+身份证,您可以一次读取它们。但是,如果您需要它们是最新的,那么每次用户更新他们的姓名/照片时,您可能必须使用云功能来更新这些引用,因此运行云功能 + 执行一些写入操作。没有“正确”/“更好”的实现,它只取决于您的用例。
    【解决方案6】:

    如果你使用的是颤振,你可以这样做:

    Firestore.instance.collection('your collection name').where(FieldPath.documentId, whereIn:[list containing multiple document IDs]).getDocuments();
    

    这将返回一个包含 List&lt;DocumentSnapshot&gt; 的 Future,您可以根据需要对其进行迭代。

    【讨论】:

    • “包含多个文档 ID 的列表”最多可以包含 10 个项目,对吗?
    【解决方案7】:

    当然,最好的方法是在 Cloud Function 中实现 Firestore 的实际查询?然后,从客户端到 Firebase 的往返电话只有一个,这似乎就是您所要求的。

    无论如何,您确实希望像服务器端一样保留所有数据访问逻辑。

    在内部,对 Firebase 本身的调用数量可能相同,但它们都将通过 Google 的超快速互连,而不是外部网络,并结合 Frank van Puffelen 解释的流水线,您应该得到这种方法表现出色。

    【讨论】:

    • 在某些逻辑复杂的情况下,将实现存储在云函数中是正确的决定,但在您只想合并具有多个 id 的列表的情况下可能不是。你失去的是客户端缓存和来自常规调用的标准化返回格式。当我使用这种方法时,在某些情况下,这导致的性能问题比在我的应用程序中解决的问题要多。
    【解决方案8】:

    对于一些陷入同样问题的人 这是一个示例代码:

    List<String> documentsIds = {your document ids};
    
    FirebaseFirestore.getInstance().collection("collection_name")
    .whereIn(FieldPath.documentId(), documentsIds).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
                @Override
                public void onComplete(@NonNull Task<QuerySnapshot> task) {
                    if (task.isSuccessful()) {
                         for (DocumentSnapshot document : Objects.requireNonNull(task.getResult())) {
                            YourClass object = document.toObject(YourClass.class);
                            // add to your custom list
                        }   
                    }
                    
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(@NonNull Exception e) {
                    e.printStackTrace();
                }
            });
    

    【讨论】:

      【解决方案9】:

      您可以使用文档 ID(最多十个)执行 IN 查询:

      import {
          query,
          collection,
          where,
          getDocs,
          documentId,
      } from 'firebase/firestore';
      
      export async function fetchAccounts(
          ids: string[]
      ) {
          // use lodash _.chunk, for example
          const result = await Promise.all(
              chunk(ids, 10).map(async (chunkIds) => {
                  const accounts = await getDocs(
                      query(
                          collection(firestore, 'accounts'),
                          where(documentId(), 'in', chunkIds)
                      ));
                  return accounts.docs.filter(doc => doc.exists()).map(doc => doc.data());
              })
          );
          return result.flat(1);
      }
      

      【讨论】:

        【解决方案10】:

        以下是在 Kotlin 中使用 Android SDK 执行此类操作的方法。
        不一定是一次往返,但它确实有效地将结果分组并避免了许多嵌套回调。

        val userIds = listOf("123", "456")
        val userTasks = userIds.map { firestore.document("users/${it!!}").get() }
        
        Tasks.whenAllSuccess<DocumentSnapshot>(userTasks).addOnSuccessListener { documentList ->
            //Do what you need to with the document list
        }
        

        请注意,获取特定文档比获取所有文档并过滤结果要好得多。这是因为 Firestore 会针对查询结果集向您收费。

        【讨论】:

        • 效果很好,正是我想要的!
        【解决方案11】:

        我希望这对你有帮助,它对我有用。

        getCartGoodsData(id) {
        
            const goodsIDs: string[] = [];
        
            return new Promise((resolve) => {
              this.fs.firestore.collection(`users/${id}/cart`).get()
                .then(querySnapshot => {
                  querySnapshot.forEach(doc => {
                    goodsIDs.push(doc.id);
                  });
        
                  const getDocs = goodsIDs.map((id: string) => {
                    return this.fs.firestore.collection('goods').doc(id).get()
                      .then((docData) => {
                        return docData.data();
                      });
                  });
        
                  Promise.all(getDocs).then((goods: Goods[]) => {
                    resolve(goods);
                  });
                });
            });
          }
        

        【讨论】:

          【解决方案12】:

          对于那些想要使用 Angular 的人来说,这里有一个例子:

          首先需要一些库导入:(必须预先安装)

          import * as firebase from 'firebase/app'
          import { AngularFirestore, AngularFirestoreCollection } from '@angular/fire/firestore'
          

          集合的一些配置:

          yourCollection: AngularFirestoreCollection;
          
          constructor(
              private _db : AngularFirestore,
          ) { 
              // this is your firestore collection
              this.yourCollection = this._db.collection('collectionName');
          }
          

          查询方法如下:('products_id' is an Array of ids)

          getProducts(products_ids) {
              var queryId = firebase.firestore.FieldPath.documentId();
              this.yourCollection.ref.where(queryId, 'in', products_ids).get()
                  .then(({ docs }) => {
                      console.log(docs.map(doc => doc.data()))
                  })
          }
          

          【讨论】:

            【解决方案13】:

            目前这在 Firestore 中似乎是不可能的。我不明白为什么亚历山大的回答被接受了,他提出的解决方案只是返回“用户”集合中的所有文档。

            根据您需要做什么,您应该考虑复制您需要显示的相关数据,并且仅在需要时请求完整的文档。

            【讨论】:

              【解决方案14】:

              是的,这是可能的。 .NET SDK for Firestore 中的示例:

              /*List of document references, for example:
                  FirestoreDb.Collection(ROOT_LEVEL_COLLECTION).Document(DOCUMENT_ID);*/
                  List<DocumentReference> docRefList = YOUR_DOCUMENT_REFERENCE_LIST;
                  
                  // Required fields of documents, not necessary while fetching entire documents
                  FieldMask fieldMask = new FieldMask(FIELD-1, FIELD-2, ...);
                  
                  // With field mask
                  List<DocumentSnapshot> documentSnapshotsMasked = await FirestoreDb.GetAllSnapshotsAsync(docRefList, fieldMask);
                  
                  // Without field mask
                  List<DocumentSnapshot>documentSnapshots = await FirestoreDb.GetAllSnapshotsAsync(docRefList);
              

              .NET 中的文档:

              1. Get all snapshots

              2. Field mask

              【讨论】:

              • 虽然此链接可能会回答问题,但最好在此处包含答案的基本部分并提供链接以供参考。如果链接页面发生更改,仅链接答案可能会失效。 - From Review
              • 是的,这完全回答了这个问题。我还添加了示例代码 sn-p 以及参考链接。
              【解决方案15】:

              使用 Firebase 版本 9(2021 年 12 月更新):

              您可以在一次往返中通过多个 id 获取多个文档,使用 "documentId()""in" >“where”子句:

              import {
                query,
                collection,
                where,
                documentId,
                getDocs
              } from "firebase/firestore";
              
              const q = query(
                collection(db, "products"),
                where(documentId(), "in", 
                  [
                    "8AVJvG81kDtb9l6BwfCa", 
                    "XOHS5e3KY9XOSV7YYMw2", 
                    "Y2gkHe86tmR4nC5PTzAx"
                  ]
                ),
              );
              
              const productsDocsSnap = await getDocs(q);
              
              productsDocsSnap.forEach((doc) => {
                console.log(doc.data()); // "doc1", "doc2" and "doc3"
              });
              

              【讨论】:

                【解决方案16】:

                您能做的最好的事情是使用Promise.all 作为您的客户端,然后必须等待.all 读取才能继续。

                迭代读取并让它们独立解决。在客户端,这可能归结为 UI 有多个进度加载器图像独立解析为值。但是,这比在.all 读取解析之前冻结整个客户端要好。

                因此,立即将所有同步结果转储到视图中,然后让异步结果在它们解析时单独进入。这似乎是微不足道的区别,但如果您的客户的互联网连接较差(就像我目前在这家咖啡店的情况一样),将整个客户体验冻结几秒钟可能会导致“这个应用很糟糕”的体验。

                【讨论】:

                • 它是异步的,有很多使用Promise.all的用例......它不一定要“冻结”任何东西——你可能需要等待所有数据才能能够做一些有意义的事情
                • 当您确实需要加载所有数据时,有几个用例,因此等待(就像带有适当消息的微调器,无需像您说的那样“冻结”任何 UI) Promise.all 完全需要......这真的取决于你在这里构建什么样的产品。在我自己看来,这类 cmets 非常无关紧要,其中不应该有任何“最好”的词。这实际上取决于一个人可能面临的每一种不同的用例以及您的应用为用户做了什么。
                猜你喜欢
                • 2021-02-12
                相关资源
                最近更新 更多