12

I'm going through the Firestore docs and guide. My code samples below use AngularFire2.

Let's consider a "chats" collection similar to the examples provided here: https://firebase.google.com/docs/firestore/manage-data/structure-data

They recommend this kind of structure but I don't see where they discuss getting all the data efficiently.

Each chat document has properties, a collection of members, and a collection of messages:

  • chatsCollection
    • chatDocument
      • [insert chat data fields here]
      • membersCollection
        • memberDocument
          • [insert member data fields here]
      • messagesCollection
        • messageDocument
          • [insert message data fields here]

Firestore queries are shallow, which could be great sometimes. My understanding is that there's no baked-in way to query deep and get nested collections. So, what are the best practices and most hassle-free ways to do this?

At the moment I'm retrieving and mapping snapshots to objects with IDs and adding the nested collection data onto the parent document data with additional queries and mappings, and I'm not happy with my approach, and could do it quicker even with denormalized Firebase structures.

This code example is just mapping on the members, adding back in messages is a whole other story...

getChatsFromFirestore() {
    this.chats = this.dataSvc.getChatsFromFirestore().snapshotChanges()
      .map(chatSnaps => {
        return chatSnaps.map(chat => {
          const chatData = chat.payload.doc.data();
          const chatId = chat.payload.doc.id;
          return this.dataSvc.getMembersForChat(chatId)
            .snapshotChanges()
            .map(memberSnaps => {
              return memberSnaps.map(member => {
                const data = member.payload.doc.data();
                const id = member.payload.doc.id;
                return { id, ...data }
              });
            })
            .map(members => {
              return { chatId, ...chatData, members: members };
            });
        })
      })
      .flatMap(chats => Observable.combineLatest(chats));
  }

And from the service:

getChatsFromFirestore() {
  return this.fsd.collection<any>('chats');
}
getChatFromFirestoreById(id: string) {
  return this.fsd.doc(`chats/${id}`);
}
getMembersForChat(chatId) {
  return this.getChatFromFirestoreById(chatId).collection('members');
}
Methodician
  • 2,396
  • 5
  • 30
  • 49
  • 1
    I guess there's an argument here for duplicating & denormalizing data as per the process for the Realtime Database (something like [this answer](https://stackoverflow.com/a/30699277/2754146)), but I'm also hoping there's a different/better process for joining data in Firestore. – Grimthorr Oct 31 '17 at 12:11
  • 1
    Good point and I did dupe & denorm the data a bit in this case. For instance, each chat contains its members which have a doc ID matching a UID which can be used to get all that member's data, but also contains the user's name. Each individual message also contains an "authorName" field and a "sentBy" field holding that user's UID again... still, being able to get all the nested data under a chatroom document without recursive calls to the DB or at least with less verbose code would be cool. – Methodician Nov 01 '17 at 20:32
  • If you save the id with `chatDocument` as a field, you can use `.valueChanges()` and get the id. which is faster than `.snapshotChanges()`. – Hareesh Nov 02 '17 at 22:33
  • How do you get only relevant chatDocuments for the user? Does every user see every chat..? – Shahar Nov 09 '17 at 07:11
  • This is just a relevant example of data from the Firebase docs. Getting relevant chatDocuments for the user would follow a slightly different path but would still require nested, sequential database queries. There may be other situations in which I'd want to get a list of documents and all their nested data at once just as in the example though. – Methodician Nov 10 '17 at 16:13
  • You can consider using project like https://github.com/lmcq/firebase-firestorm – Adam Pietrasiak Sep 19 '20 at 09:08

1 Answers1

1

The approach you posted seems like it would work and for a large chat application you probably do not want to track every single event that happens in every chatroom as that could be a lot of data. Instead it would probably be better to subscribe to only what is needed and handle periodic updates with cloud functions and cloud messaging.

By using a helper function observeCollection as well as small code restructuring it would cleanup the service and create observables for each chatroom that would be inactive until they are subscribed to.

class Service {
    // db is plan firestore / no angularfire
    db: firebase.firestore.Firestore;

    loadChatrooms() {
        const chatsRef = this.db.collection('chats');
        return observeCollection(chatsRef)
            .pipe(
                map(chats => {
                    return chats.map(chat => {
                        return {
                            chat,
                            members$: this.observeCollection(chat.ref.collection('members')),
                            messages$: this.observeCollection(chat.ref.collection('messages')),
                        };
                    })
                }),
            );
    }

    // Takes a reference and returns an array of documents
    // with the id and reference
    private observeCollection(ref) {
        return Observable.create((observer) => {
            const unsubscribeFn = ref.onSnapshot(
                snapshot => {
                    observer.next(snapshot.docs.map(doc => {
                        const data = doc.data();
                        return { 
                            ...doc.data(),
                            id: doc.id,
                            ref: doc.ref
                        };
                    }));
                },
                error => observer.error(error),
            );

            return unsubscribeFn;
        });
    }
}

In the application you could then only observe the currently selected chatrooms members and messages, which would save data. Since this post is tagged with Angular async pipes would help with switching by automatically handly subscriptisons.

In your component:

this.currentChat$ = combineLatest(
  service.loadChatrooms(),
  currentlySelectedRoomId
).pipe(
    map(([chats, selectedRoomId]) => {
        return chats.first(chat => chat.id === selectedRoomId)
    })
);

In your template:

<div *ngIf="currentChat$ as currentChat">
    {{ currentChat.name }}

    <div *ngIf="currentChat.members$ as members">
        <div *ngIf="let member of members">
          {{ member.name }}
        </div> 
    </div> 

    <div *ngIf="currentChat.messages$ as messages">
        <div *ngIf="let message of messages">
          {{ message.content }}
        </div> 
    </div> 
</div>
adamduren
  • 3,857
  • 1
  • 14
  • 10