Query and Fetch Documents from Sub Collections in Firebase Firestore
Cloud Firestore DB (offered by Firebase) supports subcollections that allows you to create “child” collections associated with a specific document. So for instance, a posts
collection may have a bunch of post documents with each of them can having a comments
subcollection, alongside its own fields.
>> posts (collection)
> postA (document ID)
title: ...
description: ...
>> comments (subcollection)
> commentA (document ID)
text: ...
commenter:
> postB (document ID)
title: ...
description: ...
>> comments (subcollection)
> commentB (document ID)
text: ...
commenter:
Referring to each post and comment document is no big deal, we can do it like this in JavaScript:
// To refer to a post document (DocumentReference)
doc(db, 'posts/postA') // Web Client SDK
db.doc('posts/postA') // Node Admin SDK
// To refer to a comments subcollection document associated with a post
doc(db, 'posts/postA/comments/commentA') // Web Client SDK
doc(db, 'posts/postB/comments/commentB')
db.doc('posts/postA/comments/commentA') // Node Admin SDK
db.doc('posts/postB/comments/commentB')
Subcollections allows us to structure our data in an intuitive hierarchy that leads to better access of documents. We can create collections anywhere in the database. At the top level of the database or inside documents (making it a subcollection). While accessing a collection (see the example below) or a document (example above), we just need to make sure that the entire reference path is specified correctly.
If we wanted to query across a comments collection associated with a particular post document, this is what we would do:
// In Node Admin SDK
const querySnapshot = await db
.collection('posts/postA/comments')
.where('commenter', '==', '...')
.get()
// In Web Client
const querySnapshot = await getDocs(
query(
collection('posts/postA/comments'),
where('commenter', '==', '...')
)
)
// Iterate through the documents fetched
querySnapshot.forEach((queryDocumentSnapshot) => {
console.log(
queryDocumentSnapshot.id,
queryDocumentSnapshot.data()
)
})
But what if we wanted to query across all the subcollections ? This is a very common usecase and the main topic for this article.
For instance we may want to query and retrieve all the comments created by a particular commenter, across ALL THE posts and not just a specific post (document). For this we would want a way to instruct firestore to:
- Go inside each post document.
- Then traverse through each comments subcollection associated with each post document.
- And finally filter out all the comment documents that belong to a particular
commenter
.
Firestore solves this exact requirement with a feature called collection group queries. Let’s see an example first:
const querySnapshot = await db
.collectionGroup('comments') // Note how this is NOT collection('...')
.where('commenter', '==', '...')
.get()
A collection group simply consists of or selects all the collections in the entire database with the ID provided (comments
). By default, when we do something like this – db.collection('foo')
– firestore queries and retrieves data from a single collection. Where as when we use a collection group like this – db.collectionGroup('foo')
– firestore will query all the collections (and subcollections) present everywhere with the name foo
to fetch the documents.
// db.collectionGroup('foo').get() will get both fooA and fooB
>> foo (collection)
> fooA (document)
field1: ...
field2: ...
>> posts (collection)
> postA (document)
title: ...
description: ...
>> foo (subcollection)
> fooB (document)
field1: ...
field2: ...
That explains how we can query across all subcollections of the same name/type. Let’s look at one more problem.
In the collectionGroup
snippet above, we saw how to get all the comments for a commenter. But what if we wanted all the posts of all those comments as well ? To simplify the problem statement, how can we get all the posts where a commenter has commented. This is not directly possible in firestore. We’d have to fetch the comments first (as we saw earlier) and then load the parent post document for each comment document’s containing collection.
For every comment document fetched, we can access its parent path via the ref
property. Something like this:
// Fetch the comments first
const querySnapshot = await db
.collectionGroup('comments')
.where('commenter', '==', '...')
.get()
querySnapshot.forEach((queryDocumentSnapshot) => {
const documentReference = queryDocumentSnapshot.ref; // DocumentReference
const documentParent = documentReference.parent; // CollectionReference
// documentReference.path - Reference path of this (comment) document
//
// documentParent.id - Parent (comment) collection's ID
// documentParent.path - Path of the parent (comment) collection
// documentParent.parent (DocumentReference) - Parent (post) document of parent (comment) collection (grand-parent)
})
With the availability of parent information, that can be chained (.parent.parent.parent...
) we can go higher up in the hierarchy – to the post document, the post collection, so on. Notice how the reference type alternates as we go up the chain:
const collectionRef1 = documentRef1.parent // CollectionReference
const documentRef2 = collectionRef1.parent // DocumentReference
const collectionRef2 = documentRef2.parent // CollectionReference
const documentRef3 = collectionRef2.parent // DocumentReference
...
If you want, check out the documentation for CollectionReference
and DocumentReference
.
A note on collection group scope indexes
If we simply fetch all the documents from all the collections of a “group” (collectionGroup
) then we don’t need any index. But in most cases we will either order by or filter on a particular field. Just like we filtered on the commenter
field in our examples above. For this we must manually create a collection group scope index that firestore will use to respond to the incoming collection group queries.
It is important to understand that firestore serves all its read queries from indexes to achieve high performance. For all the fields in a document, it automatically creates a single-field index but it doesn’t automatically create composite indexes (multiple fields) and collection group indexes. Hence we must manually create one so that firestore can have a single place (index) to map a field’s value to all the different collection/document
and subcollection/document
locations.
You can either go to Firestore Databases > Indexes
console and do it, or use the firebase cli. But the easiest option is to just let your code (that performs the query) run and firestore will automatically error out when an index is missing. The error message will contain a link (to the same console) to create the index. This process is much simpler and lazy!