Cloud Firestore deep get with subcollection

DatabaseFirebaseGoogle Cloud-PlatformGoogle Cloud-FirestoreData Modeling

Database Problem Overview


Let's say we have a root collection named 'todos'.

Every document in this collection has:

  1. title: String
  2. subcollection named todo_items

Every document in the subcollection todo_items has

  1. title: String
  2. completed: Boolean

I know that querying in Cloud Firestore is shallow by default, which is great, but is there a way to query the todos and get results that include the subcollection todo_items automatically?

In other words, how do I make the following query include the todo_items subcollection?

db.collection('todos').onSnapshot((snapshot) => {
  snapshot.docChanges.forEach((change) => {
    // ...
  });
});

Database Solutions


Solution 1 - Database

This type of query isn't supported, although it is something we may consider in the future.

Solution 2 - Database

If anyone is still interested in knowing how to do deep query in firestore, here's a version of cloud function getAllTodos that I've come up with, that returns all the 'todos' which has 'todo_items' subcollection.

exports.getAllTodos = function (req, res) {
    getTodos().
        then((todos) => {
            console.log("All Todos " + todos) // All Todos with its todo_items sub collection.
            return res.json(todos);
        })
        .catch((err) => {
            console.log('Error getting documents', err);
            return res.status(500).json({ message: "Error getting the all Todos" + err });
        });
}

function getTodos(){
    var todosRef = db.collection('todos');

    return todosRef.get()
        .then((snapshot) => {
            let todos = [];
            return Promise.all(
                snapshot.docs.map(doc => {  
                        let todo = {};                
                        todo.id = doc.id;
                        todo.todo = doc.data(); // will have 'todo.title'
                        var todoItemsPromise = getTodoItemsById(todo.id);
                        return todoItemsPromise.then((todoItems) => {                    
                                todo.todo_items = todoItems;
                                todos.push(todo);         
                                return todos;                  
                            }) 
                })
            )
            .then(todos => {
                return todos.length > 0 ? todos[todos.length - 1] : [];
            })
            
        })
}


function getTodoItemsById(id){
    var todoItemsRef = db.collection('todos').doc(id).collection('todo_items');
    let todo_items = [];
    return todoItemsRef.get()
        .then(snapshot => {
            snapshot.forEach(item => {
                let todo_item = {};
                todo_item.id = item.id;
                todo_item.todo_item = item.data(); // will have 'todo_item.title' and 'todo_item.completed'             
                todo_items.push(todo_item);
            })
            return todo_items;
        })
}

Solution 3 - Database

You could try something like this:

db.collection('coll').doc('doc').collection('subcoll').doc('subdoc')

Solution 4 - Database

I have faced the same issue but with IOS, any way if i get your question and if you use auto-ID for to-do collection document its will be easy if your store the document ID as afield with the title field in my case :

let ref = self.db.collection("collectionName").document()

let data  = ["docID": ref.documentID,"title" :"some title"]

So when you retrieve lets say an array of to-do's and when click on any item you can navigate so easy by the path

ref = db.collection("docID/\(todo_items)")

I wish i could give you the exact code but i'm not familiar with Javascript

Solution 5 - Database

I used AngularFirestore (afs) and Typescript:

import { map, flatMap } from 'rxjs/operators';
import { combineLatest } from 'rxjs';

interface DocWithId {
  id: string;
}

convertSnapshots<T>(snaps) {
  return <T[]>snaps.map(snap => {
    return {
      id: snap.payload.doc.id,
      ...snap.payload.doc.data()
    };
  });
}

getDocumentsWithSubcollection<T extends DocWithId>(
    collection: string,
    subCollection: string
  ) {
    return this.afs
      .collection(collection)
      .snapshotChanges()
      .pipe(
        map(this.convertSnapshots),
        map((documents: T[]) =>
          documents.map(document => {
            return this.afs
             .collection(`${collection}/${document.id}/${subCollection}`)
              .snapshotChanges()
              .pipe(
                map(this.convertSnapshots),
                map(subdocuments =>
                  Object.assign(document, { [subCollection]: subdocuments })
                )
              );
          })
        ),
        flatMap(combined => combineLatest(combined))
      );
  }
  

Solution 6 - Database

As pointed out in other answers, you cannot request deep queries.

My recommendation: Duplicate your data as minimally as possible.

I'm running into this same problem with "pet ownership". In my search results, I need to display each pet a user owns, but I also need to be able to search for pets on their own. I ended up duplicated the data. I'm going to have a pets array property on each user AS WELL AS a pets subcollection. I think that's the best we can do with these kinds of scenarios.

Solution 7 - Database

According to docs, you needs to make 2 calls to the firestore.. one to fetch doc and second to fetch subcollection. The best you can do to reduce overall time is to make these two calls parallelly using promise.All or promise.allSettled instead of sequentially.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionqeroqazoView Question on Stackoverflow
Solution 1 - DatabaseDan McGrathView Answer on Stackoverflow
Solution 2 - DatabaseMalickView Answer on Stackoverflow
Solution 3 - DatabaseSajidh ZahirView Answer on Stackoverflow
Solution 4 - DatabaseMohammed RiyadhView Answer on Stackoverflow
Solution 5 - Databaseuser2734839View Answer on Stackoverflow
Solution 6 - Databaseblueberry_chopsticksView Answer on Stackoverflow
Solution 7 - DatabaseGorvGoylView Answer on Stackoverflow