如何在 Cloud Firestore 查询中加入多个文档?

Posted

技术标签:

【中文标题】如何在 Cloud Firestore 查询中加入多个文档?【英文标题】:How to join multiple documents in a Cloud Firestore query? 【发布时间】:2018-06-05 11:17:49 【问题描述】:

我有一个具有以下结构的 Cloud Firestore 数据库:

用户 [uid] 名称:“测试用户” 帖子 [编号] 内容:“只是一些测试帖。” 时间戳:(2017 年 12 月 22 日) uid:[uid]

实际数据库中存在更多数据,以上只是说明了集合/文档/字段结构。

我的 Web 应用中有一个视图,我在其中显示帖子,并希望显示发帖用户的姓名。我正在使用以下查询来获取帖子:

let loadedPosts = ;
posts = db.collection('posts')
          .orderBy('timestamp', 'desc')
          .limit(3);
posts.get()
.then((docSnaps) => 
  const postDocs = docSnaps.docs;
  for (let i in postDocs) 
    loadedPosts[postDocs[i].id] = postDocs[i].data();
  
);

// Render loadedPosts later

我要做的是通过存储在帖子的uid字段中的uid查询用户对象,并将用户的名称字段添加到相应的loadedPosts对象中。如果我一次只加载一篇文章,这没问题,只需等待查询返回一个对象,然后在 .then() 函数中对用户文档进行另一次查询,依此类推。

但是,由于我一次获取多个帖子文档,因此在对每个帖子的 user/[uid] 文档调用 .get() 后,由于异步,我很难弄清楚如何将正确的用户映射到正确的帖子他们返回的方式。

谁能想到一个优雅的解决方案来解决这个问题?

【问题讨论】:

medium.com/google-developer-experts/… 链接到我在另一个帖子中的答案***.com/a/51671175/7212651 【参考方案1】:

对我来说似乎相当简单:

let loadedPosts = ;
posts = db.collection('posts')
          .orderBy('timestamp', 'desc')
          .limit(3);
posts.get()
.then((docSnaps) => 
  docSnaps.forEach((doc) => 
    loadedPosts[doc.id] = doc.data();
    db.collection('users').child(doc.data().uid).get().then((userDoc) => 
      loadedPosts[doc.id].userName = userDoc.data().name;
    );
  )
);

如果你想防止多次加载一个用户,你可以缓存用户数据客户端。在这种情况下,我建议将用户加载代码分解为辅助函数。但这将是上述内容的变体。

【讨论】:

哦,太好了,我刚刚测试过,效果很好!我在想,如果我这样处理它,doc.id 最终会成为每次调用用户 get().then() 的最后一次迭代文档的 id,我误解了在循环中定义回调! 当页面上有 n 个帖子时,这会导致 1+n 次数据库调用。有没有办法在一个数据库调用中下载收到的帖子的所有用户名? 不,在客户端 SDK 中没有这样的选项。见***.com/questions/46721517/… re:“您可以缓存用户数据客户端” - 我相信如果您附加一个侦听器而不是使用 .get(),Firebase SDK 将处理缓存数据,您可以问随心所欲地使用它,并始终获取缓存的数据。如果您订阅的数据发生更改,缓存本身将更新...【参考方案2】:

我会进行 1 个用户文档调用和所需的帖子调用。

let users =  ;
let loadedPosts = ;
db.collection('users').get().then((results) => 
  results.forEach((doc) => 
    users[doc.id] = doc.data();
  );
  posts = db.collection('posts').orderBy('timestamp', 'desc').limit(3);
  posts.get().then((docSnaps) => 
    docSnaps.forEach((doc) => 
    loadedPosts[doc.id] = doc.data();
    loadedPosts[doc.id].userName = users[doc.data().uid].name;
  );
); 

【讨论】:

2 次调用而不是 4 次,很好! 这会加载 所有 用户,而不仅仅是撰写最后 3 个帖子的用户。虽然这在开发过程中可能会正常工作,但随着您添加更多用户,您将加载越来越多的不必要数据。 这不是一个好的解决方案。如果您的数据库中有一千个用户怎么办?您必须加载一千个用户文档才能获得 3 个所需的用户? 上面的代码可以用Cloud Function写吗?【参考方案3】:

在尝试了多种解决方案后,我使用 RXJS combineLatest 完成了它,取操作员。使用 map 函数我们可以组合结果。

可能不是最佳解决方案,但它可以解决您的问题。

combineLatest(
    this.firestore.collection('Collection1').snapshotChanges(),
    this.firestore.collection('Collection2').snapshotChanges(),
    //In collection 2 we have document with reference id of collection 1
)
.pipe(
    take(1),
).subscribe(
    ([dataFromCollection1, dataFromCollection2]) => 

        this.dataofCollection1 = dataFromCollection1.map((data) => 
            return 
                id: data.payload.doc.id,
                ...data.payload.doc.data() as ,
            
            as IdataFromCollection1;
        );

        this.dataofCollection2 = dataFromCollection2.map((data2) => 
            return 
                id: data2.payload.doc.id,
                ...data2.payload.doc.data() as ,
            
            as IdataFromCollection2;
        );

        console.log(this.dataofCollection2, 'all feeess');

        const mergeDataFromCollection =
            this.dataofCollection1.map(itm => (
                payment: [this.dataofCollection2.find((item) => (item.RefId === itm.id))],
                ...itm
            ))

        console.log(mergeDataFromCollection, 'all data');
    ,

【讨论】:

【参考方案4】:

我的解决方案如下。

概念:您知道要获取信息的用户ID,在您的帖子列表中,您可以请求用户文档并将其保存为您的帖子项目中的承诺。承诺解决后,您将获得用户信息。

注意:我不测试下面的代码,但它是我的代码的简化版本。

let posts: Observable<[]>;  // you can display in html directly with  | async tag
this.posts = this.listenPosts()
         .map( posts => 
           posts.forEach( post => 
                post.promise = this.getUserDoc( post.uid )
                               .then( (doc: DocumentSnapshot) => 
                                  post.userName = doc.data().name;
                               );
           ); // end forEach
           return posts;
         );

// normally, i keep in provider
listenPosts(): Observable<any> 
  let fsPath = 'posts';
  return this.afDb.collection( fsPath ).valueChanges();


// to get the document according the user uid
getUserDoc( uid: string ): Promise<any> 
   let fsPath = 'users/' + uid;
   return this.afDb.doc( fsPath ).ref.get();

注意:afDb:AngularFirestore 在构造函数中初始化(通过 angularFire 库)

【讨论】:

【参考方案5】:

如果您想加入 observables 而不是 Promise,请使用 combineLatest。以下是将 user 文档连接到 post 文档的示例:

  getPosts(): Observable<Post[]> 
    let data: any;
    return this.afs.collection<Post>('posts').valueChanges().pipe(
      switchMap((r: any[]) => 
        data = r;
        const docs = r.map(
          (d: any) => this.afs.doc<any>(`users/$d.user`).valueChanges()
        );
        return combineLatest(docs).pipe(
          map((arr: any) => arr.reduce((acc: any, cur: any) => [acc].concat(cur)))
        );
      ),
      map((d: any) => 
        let i = 0;
        return d.map(
          (doc: any) => 
            const t =  ...data[i], user: doc ;
            ++i;
            return t;
          
        );
      )
    );
  

此示例将每个文档连接到集合中,但如果您只想将一个文档连接到另一个文档,则可以简化此操作。

这假设您的 post 文档有一个 user 变量,其中包含文档的 userId

J

【讨论】:

以上是关于如何在 Cloud Firestore 查询中加入多个文档?的主要内容,如果未能解决你的问题,请参考以下文章

从 Cloud Firestore 在 MatDatepicker 中加载时间戳 |火力基地

如何在 Cloud Firestore 中使用文档 ID 执行集合组查询

查询时如何在 Flutter (Dart) 中使用变量 Cloud Firestore

如何使用 Cloud Firestore 查询用户待处理的好友请求?

如何组合两个 Redux Firebase Cloud Firestore 查询?

Cloud Firestore:如何在我的集合查询中获取文档引用并将其映射为 JSON 值?