简体   繁体   中英

How to join multiple documents in a Cloud Firestore query?

I have a Cloud Firestore DB with the following structure:

  • users
    • [uid]
      • name: "Test User"
  • posts
    • [id]
      • content: "Just some test post."
      • timestamp: (Dec. 22, 2017)
      • uid: [uid]

There is more data present in the actual DB, the above just illustrates the collection/document/field structure.

I have a view in my web app where I'm displaying posts and would like to display the name of the user who posted. I'm using the below query to fetch the posts:

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

What I want to do is query the user object by the uid stored in the post's uid field, and add the user's name field into the corresponding loadedPosts object. If I was only loading one post at a time this would be no problem, just wait for the query to come back with an object and in the .then() function make another query to the user document, and so on.

However because I'm getting multiple post documents at once, I'm having a hard time figuring out how to map the correct user to the correct post after calling .get() on each post's user/[uid] document due to the asynchronous way they return.

Can anyone think of an elegant solution to this issue?

It seems fairly simple to me:

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;
    });
  })
});

If you want to prevent loading a user multiple times, you can cache the user data client side. In that case I'd recommend factoring the user-loading code into a helper function. But it'll be a variation of the above.

I would do 1 user doc call and the needed posts call.

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;
  });
}); 

After trying multiple solution I get it done with RXJS combineLatest, take operator. Using map function we can combine result.

Might not be an optimum solution but here its solve your problem.

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');
    },

my solution as below.

Concept: You know user id you want to get information, in your posts list, you can request user document and save it as promise in your post item. after promise resolve then you get user information.

Note: i do not test below code, but it is simplify version of my code.

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();
}

Note: afDb: AngularFirestore it is initialize in constructor (by angularFire lib)

If you want to join observables instead of promises, use combineLatest . Here is an example joining a user document to a post document:

  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;
          }
        );
      })
    );
  }

This example joins each document in a collection, but you could simplify this if you wanted to just join one single document to another.

This assumes your post document has a user variable with the userId of the document.

J

I will do just one userdoc query and the needed postsdoc call.

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') 
posts.get().then((docSnaps)=>{
docSnaps.forEach((doc)=>{
loadedPosts[doc.id] = doc.data();
loadedPosts[doc.id].userName = users[doc.data().uid].name});
}) 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM