IX***
countries<span class="hljs-regexp">/UK/</span>largestCities<span class="hljs-regexp">/LDN/</span>landmarks/Qs6iR3BYSuOF7gTZo***
countries<span class="hljs-regexp">/UK/</span>largestCities<span class="hljs-regexp">/MCR/</span>landmarks/HXbzW9rwPgtMr7Ohw***
countries<span class="hljs-regexp">/UK/</span>smallestCities<span class="hljs-regexp">/CRD/</span>landmarks/Q3UmT9hTBnHgs7wAY***
countries<span class="hljs-regexp">/US/</span>largestCities<span class="hljs-regexp">/NY/</span>landmarks/L07Dzgv97kcYpBFQO***</pre></div><p id="e64f">Meaning that we get <b>all</b> landmarks of <b>all</b> countries. Hold on, but this is <b>not</b> what we want. We need to get only the landmarks from a <b>single</b> country, for example, from the <b>UK</b>.</p><h2 id="2951">How can we do that?</h2><p id="d9d1">A relatively simple solution would be to add the <b>Country,</b> as a property of the Landmark object like this:</p><div id="611c"><pre>Firestore-root
|<span class="hljs-string">
--- countries (collection)
</span>|
--- UK (document)
|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- name: "United Kingdom"
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- largestCities (sub-collection)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- LDN (document)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- name: "London"
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- landmarks (sub-collection)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- landmarkId (document)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- name: "Big Ben"
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- country: "UK" //Newly added
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- landmarkId (document)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- name: "Buckingham Palace"
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|<span class="hljs-string"> --- country: "UK" //Newly added
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- MCR (document)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- name: "Manchester"
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- landmarks (sub-collection)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- landmarkId (document)
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- name: "Manchester Library"
</span>|<span class="hljs-string"> </span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> </span>|<span class="hljs-string"> --- country: "UK" //Newly added
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- smalltesCities (sub-collection)
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- CRD (document)
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- name: "Cardiff"
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- landmarks (sub-collection)
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- landmarkId (document)
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- name: "Cardiff Castle"
</span>|<span class="hljs-string"> </span>|
|<span class="hljs-string"> --- country: "UK" //Newly added
</span>|
--- US (document)
|<span class="hljs-string">
--- name: "United States"
</span>|
--- largestCities (sub-collection)
|<span class="hljs-string">
--- NY (document)
</span>|
--- name: <span class="hljs-string">"New York"</span>
|<span class="hljs-string">
--- landmarks (sub-collection)
</span>|
--- $landmarkId (document)
|<span class="hljs-string">
--- name: "Central Park"
</span>|
--- country: <span class="hljs-string">"US"</span> //Newly added</pre></div><p id="c3f5">And perform the following query:</p><div id="0e7c"><pre>rootRef<span class="hljs-selector-class">.collectionGroup</span>(<span class="hljs-string">"landmarks"</span>)
<span class="hljs-selector-class">.whereEqualTo</span>(<span class="hljs-string">"country"</span>, <span class="hljs-string">"UK"</span>)
<span class="hljs-selector-class">.get</span>()
<span class="hljs-selector-class">.addOnCompleteListener</span> { <span class="hljs-comment">/* ... */</span> }</pre></div><p id="9a74">But is this the best solution? The simplest answer is, <b>no</b>! Why? Because of the following two main reasons:</p><ol><li>To perform such a query, an <b>index</b> is required. This can be done, by creating it manually in the <a href="https://console.firebase.google.com/">Firebase Console</a>, or you’ll find in your logs a message that sounds like this:</li></ol><div id="0768"><pre>FAILED_PRECONDITION: The query requires <span class="hljs-keyword">an</span> index. You can <span class="hljs-built_in">create</span> <span class="hljs-keyword">it</span> here: <span class="hljs-keyword">https</span>://...</pre></div><p id="e1ad">You can simply click on that link, or copy and paste the URL into a web browser and the index will be created automatically. However, there is a downside. Currently, there is <b>no way</b> you can create such an index <b>programmatically</b>. Even if you create the required index directly in the Firebase Console, it won’t help, since the names of the countries are dynamically added, and <b>manually </b>creating an index for each country<b>,</b> doesn’t sound like an option. Besides that, it may also lead you to reach the <a hre
Options
f="https://firebase.google.com/docs/firestore/quotas#indexes">maximum number of indexes</a> very quickly.</p><blockquote id="382b"><p>Maximum number of composite indexes for a database: <b>200</b></p></blockquote><p id="0ae5">After building the index, we get the following result, without having the <b>US</b> in the results:</p><div id="b382"><pre>countries<span class="hljs-regexp">/UK/</span>largestCities<span class="hljs-regexp">/LDN/</span>landmarks/QcTj6X5Upqa4gWfIX***
countries<span class="hljs-regexp">/UK/</span>largestCities<span class="hljs-regexp">/LDN/</span>landmarks/Qs6iR3BYSuOF7gTZo***
countries<span class="hljs-regexp">/UK/</span>largestCities<span class="hljs-regexp">/MCR/</span>landmarks/HXbzW9rwPgtMr7Ohw***
countries<span class="hljs-regexp">/UK/</span>smallestCities<span class="hljs-regexp">/CRD/</span>landmarks/Q3UmT9hTBnHgs7wAY***</pre></div><p id="eb5c">2. Since we know that there are some limitations when it comes to how much data we store in a single document, according to the official documentation regarding <a href="https://firebase.google.com/docs/firestore/quotas">usage and limits</a>:</p><blockquote id="9aac"><p>Maximum size for a document: <b>1 MiB</b> (1,048,576 bytes)</p></blockquote><p id="ce09">So adding more data into the document might also lead to reaching this limitation too.</p><h2 id="b33f">So is this the single option we have?</h2><p id="d3c8"><b>No, it’s not!</b> The key to solving this issue without the above limitations is to use a collection group query for finding all matching document paths that <b>start</b> with the given country’s document <b>path</b> like this:</p><div id="eaf3"><pre>val countriesRef = rootRef<span class="hljs-selector-class">.collection</span>(<span class="hljs-string">"countries"</span>)
val ukDocRef = countriesRef<span class="hljs-selector-class">.document</span>(<span class="hljs-string">"UK"</span>)
rootRef<span class="hljs-selector-class">.collectionGroup</span>(<span class="hljs-string">"landmarks"</span>)
<span class="hljs-selector-class">.orderBy</span>(FieldPath<span class="hljs-selector-class">.documentId</span>())
<span class="hljs-selector-class">.startAt</span>(ukDocRef.path)
<span class="hljs-selector-class">.endAt</span>(ukDocRef<span class="hljs-selector-class">.path</span> + <span class="hljs-string">"\uf8ff"</span>)
<span class="hljs-selector-class">.get</span>()
<span class="hljs-selector-class">.addOnCompleteListener</span> {
it<span class="hljs-selector-class">.apply</span> {
<span class="hljs-keyword">if</span> (isSuccessful) {
<span class="hljs-keyword">for</span> (document <span class="hljs-keyword">in</span> result) {
Log<span class="hljs-selector-class">.d</span>(TAG, document<span class="hljs-selector-class">.reference</span>.path)
}
} <span class="hljs-keyword">else</span> {
Log<span class="hljs-selector-class">.d</span>(TAG, <span class="hljs-string">"Error: "</span>, exception)
}
}
}</pre></div><p id="f67d">The result of the above code is the same as before, which is all the landmarks that correspond <b>only</b> to the <b>UK</b>. Please also note that the <code>\uf8ff</code> is the last character in Unicode, so acts as an end guard.</p><h2 id="9637">Benefits:</h2><ol><li><b>No</b> index is required.</li><li><b>Fewer</b> data to be added in the document. This means the document will have a smaller size. If you are worried about the size of the document by chance, you can always check against the maximum <b>1 MiB</b> quota using the <a href="https://github.com/alexmamo/FirestoreDocument-Android/tree/master/firestore-document">FirestoreDocument-Android</a> library.</li></ol><p id="4935">However, there is a <b>downside</b>. Considering having the following “new” country:</p><div id="433b"><pre><span class="hljs-params">---</span> USA <span class="hljs-params">(document)</span> <span class="hljs-string">//Newly</span> added
|
<span class="hljs-params">---</span> name: <span class="hljs-string">"United States of America"</span>
|
<span class="hljs-params">---</span> largestCities <span class="hljs-params">(sub-collection)</span>
|
<span class="hljs-params">---</span> SF <span class="hljs-params">(document)</span>
|
<span class="hljs-params">---</span> name: <span class="hljs-string">"San Francisco"</span>
|
<span class="hljs-params">---</span> landmarks <span class="hljs-params">(sub-collection)</span>
|
<span class="hljs-params">---</span> $landmarkId <span class="hljs-params">(document)</span>
|
<span class="hljs-params">---</span> name: <span class="hljs-string">"Golden Gate Bridge"</span></pre></div><p id="ecae">The result of the following query:</p><div id="adc2"><pre>val countriesRef = rootRef<span class="hljs-selector-class">.collection</span>(<span class="hljs-string">"countries"</span>)
val usDocRef = countriesRef<span class="hljs-selector-class">.document</span>(<span class="hljs-string">"US"</span>) <span class="hljs-comment">//Changed from UK to US</span>
rootRef<span class="hljs-selector-class">.collectionGroup</span>(<span class="hljs-string">"landmarks"</span>)
<span class="hljs-selector-class">.orderBy</span>(FieldPath<span class="hljs-selector-class">.documentId</span>())
<span class="hljs-selector-class">.startAt</span>(usDocRef.path)
<span class="hljs-selector-class">.endAt</span>(usDocRef<span class="hljs-selector-class">.path</span> + <span class="hljs-string">"\uf8ff"</span>)
<span class="hljs-selector-class">.get</span>()
<span class="hljs-selector-class">.addOnCompleteListener</span> { <span class="hljs-comment">/* ... /</span> }</pre></div><p id="39d2">Will be:</p><div id="014c"><pre>countries<span class="hljs-regexp">/US/</span>largestCities<span class="hljs-regexp">/NY/</span>landmarks/L07Dzgv97kcYpBFQO**
countries<span class="hljs-regexp">/USA/</span>largestCities<span class="hljs-regexp">/SF/</span>landmarks/<span class="hljs-number">8</span>naAYp5MrHwuJNBIq***</pre></div><p id="dcaa">And <b>not</b> only:</p><div id="d759"><pre>countries<span class="hljs-regexp">/US/</span>largestCities<span class="hljs-regexp">/NY/</span>landmarks/L07Dzgv97kcYpBFQO***</pre></div><p id="c644">As you probably expected. So be aware of this constraint. However, there is a solution that can help us get over this limitation, which would be to use only document IDs that are generated either by the <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/CollectionReference">CollectionReference</a>’s <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/CollectionReference#add(java.lang.Object)">add(Object data)</a> method or by the <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/DocumentReference">DocumentReference</a>’s <a href="https://firebase.google.com/docs/reference/android/com/google/firebase/firestore/DocumentReference#set(java.lang.Object)">set(Object data)</a> method. In this way, you’ll always have totally <b>different</b> document IDs having the same length and the queries will work as usual without the chances of hotspots.</p><h1 id="5a76">Conclusion</h1><p id="0d91">I tried to explain in this article the simplest way that currently exists for querying multiple collections in Firestore that exist under a certain path. So I hope you found this article useful and if you have any questions regarding this topic, feel free and leave a comment in the section below.</p><p id="d6b1">If you wanna support me, please <a href="https://medium.com/@alex.mamo/membership"><b>join me</b></a>!</p><p id="230b">You can see it on youtube:</p>
<figure id="88e5">
<div>
<div>
<img class="ratio" src="http://placehold.it/16x9">
<iframe class="" src="https://cdn.embedly.com/widgets/media.html?src=https%3A%2F%2Fwww.youtube.com%2Fembed%2Ft9MzSxEPcTE%3Ffeature%3Doembed&display_name=YouTube&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Dt9MzSxEPcTE&image=https%3A%2F%2Fi.ytimg.com%2Fvi%2Ft9MzSxEPcTE%2Fhqdefault.jpg&key=a19fcc184b9711e1b4764040d3dc5c07&type=text%2Fhtml&schema=youtube" allowfullscreen="" frameborder="0" height="480" width="854">
</div>
</div>
</figure></iframe></div></div></figure><p id="de59">Credit to <a href="undefined">Samuel Jones</a></p><p id="bba4">#BetterTogether 🔥</p></article></body>
How to query collections in Firestore under a certain path?
A simple solution to get content across different sub-collections
Cloud Firestore is a flexible, scalable database for mobile, web, and server development from Firebase and Google Cloud. It consists of collections and documents. When working with such a database, there often are cases in which we would like to query multiple collections at once. But unfortunately, this is not possible. And it makes sense since the queries in Firestore are shallow, meaning that can only get documents from the collection that the query is run against. There is no way to get documents from different collections or sub-collections in a single query. Firestore doesn’t support queries across different collections in one go unless we are using a collection group query.
But how about limiting the results only to sub-collections that exist under a particular document? So I’m going to explain in this article, the most simple and efficient way for solving this problem.
Let’s assume we have an application that displays landmarks from all over the world, from the smallest to the largest cities. So let’s take a simple example of a Firestore Database structure for such an app:
Please ignore the abbreviations, as I only used them as examples. I really don’t know if it actually exists.
As you can see, there is a top-level collection called “countries”. Each Country document within this collection contains a sub-collection called largestCitiesand another one called smallestCities. Within each City document, there is another sub-collection called “landmarks” that contains Landmark objects. In short, it looks like this:
How to get all landmarks of a particular country, no matter the size of the city?
Since Cloud Firestore version 19.0.0, when the collectionGroup(String collectionId) method was introduced, we are allowed to perform a query that will include all the documents in the database that are contained in a collection or subcollection with the given collectionId.
So assuming we have the following root reference:
val rootRef = FirebaseFirestore.getInstance()
We can perform a collection group query like this:
Meaning that we get all landmarks of all countries. Hold on, but this is not what we want. We need to get only the landmarks from a single country, for example, from the UK.
How can we do that?
A relatively simple solution would be to add the Country, as a property of the Landmark object like this:
But is this the best solution? The simplest answer is, no! Why? Because of the following two main reasons:
To perform such a query, an index is required. This can be done, by creating it manually in the Firebase Console, or you’ll find in your logs a message that sounds like this:
FAILED_PRECONDITION: The query requires an index. You can createit here: https://...
You can simply click on that link, or copy and paste the URL into a web browser and the index will be created automatically. However, there is a downside. Currently, there is no way you can create such an index programmatically. Even if you create the required index directly in the Firebase Console, it won’t help, since the names of the countries are dynamically added, and manually creating an index for each country, doesn’t sound like an option. Besides that, it may also lead you to reach the maximum number of indexes very quickly.
Maximum number of composite indexes for a database: 200
After building the index, we get the following result, without having the US in the results:
2. Since we know that there are some limitations when it comes to how much data we store in a single document, according to the official documentation regarding usage and limits:
Maximum size for a document: 1 MiB (1,048,576 bytes)
So adding more data into the document might also lead to reaching this limitation too.
So is this the single option we have?
No, it’s not! The key to solving this issue without the above limitations is to use a collection group query for finding all matching document paths that start with the given country’s document path like this:
val countriesRef = rootRef.collection("countries")
val ukDocRef = countriesRef.document("UK")
rootRef.collectionGroup("landmarks")
.orderBy(FieldPath.documentId())
.startAt(ukDocRef.path)
.endAt(ukDocRef.path + "\uf8ff")
.get()
.addOnCompleteListener {
it.apply {
if (isSuccessful) {
for (document in result) {
Log.d(TAG, document.reference.path)
}
} else {
Log.d(TAG, "Error: ", exception)
}
}
}
The result of the above code is the same as before, which is all the landmarks that correspond only to the UK. Please also note that the \uf8ff is the last character in Unicode, so acts as an end guard.
Benefits:
No index is required.
Fewer data to be added in the document. This means the document will have a smaller size. If you are worried about the size of the document by chance, you can always check against the maximum 1 MiB quota using the FirestoreDocument-Android library.
However, there is a downside. Considering having the following “new” country:
--- USA (document)//Newly added
|
--- name: "United States of America"
|
--- largestCities (sub-collection)
|
--- SF (document)
|
--- name: "San Francisco"
|
--- landmarks (sub-collection)
|
--- $landmarkId (document)
|
--- name: "Golden Gate Bridge"
The result of the following query:
val countriesRef = rootRef.collection("countries")
val usDocRef = countriesRef.document("US") //Changed from UK to US
rootRef.collectionGroup("landmarks")
.orderBy(FieldPath.documentId())
.startAt(usDocRef.path)
.endAt(usDocRef.path + "\uf8ff")
.get()
.addOnCompleteListener { /* ... */ }
As you probably expected. So be aware of this constraint. However, there is a solution that can help us get over this limitation, which would be to use only document IDs that are generated either by the CollectionReference’s add(Object data) method or by the DocumentReference’s set(Object data) method. In this way, you’ll always have totally different document IDs having the same length and the queries will work as usual without the chances of hotspots.
Conclusion
I tried to explain in this article the simplest way that currently exists for querying multiple collections in Firestore that exist under a certain path. So I hope you found this article useful and if you have any questions regarding this topic, feel free and leave a comment in the section below.