Let's say I have a collection of comments. Every comment object has a "doc ref" to the user who posted. I need a query that will return a list of comments including the value of every single user reference, so my query returns a nice formatted of Json comment objects.
6 Answers
A similar question was asked here What is Firebase Firestore 'Reference' data type good for?, I don't think it is possible to do what you are asking according to this answer https://stackoverflow.com/a/46570119/473453.
You have to load every reference yourself, e.g.
const comments = []
firebase.firestore().collection('/comments').get().then(snapshot => {
snapshot.docs.forEach(doc => {
const comment = doc.data()
comment.userRef.get().then(snap => {
comment.user = snap.data()
comments.push(comment)
})
})
})
For many comments this will add a lot of overhead. Maybe you can write a CloudFunction that does the work for you all on the server side and returns you a formatted JSON.
It looks like they might e working on supporting this in the future though: https://stackoverflow.com/a/46614683/473453

- 4,378
- 3
- 23
- 42

- 894
- 2
- 11
- 23
This annoyed the hell out of me as well. I made a utility helper, that can automatically populate the first level.
Helper
import { get, set } from 'lodash'
const getReference = async documentReference => {
const res = await documentReference.get()
const data = res.data()
if (data && documentReference.id) {
data.uid = documentReference.id
}
return data
}
const hydrate = async (document, paths = []) => Promise.all(
paths.map(async path => {
const documentReference = get(document, path)
if (!documentReference || !documentReference.path) {
return console.warn(
`Error hydrating documentReference for path "${path}": Not found or invalid reference`
)
}
const result = await getReference(documentReference)
set(document, path, result)
})
)
export { hydrate }
Example usage:
getUser = async uid => {
return this.db
.collection('users')
.doc(uid)
.get()
.then(async documentSnapshot => {
const data = documentSnapshot.data()
await hydrate(data, ['company', 'someOtherNestedRef-2', 'someOtherNestedRef-1'])
return data
})
}
Limitation: This example will not work for deep nested references

- 4,378
- 3
- 23
- 42

- 8,300
- 11
- 48
- 67
-
This was really helpful! Thanks Chris. – kingisaac95 Apr 29 '21 at 11:49
I suggest you duplicate the user data in each comment. Create a field in the comment doc that is an object named user
and provide the minimum amount of information required to display the comment. So your comment
doc might look like...
{
id: 'CEXwQAHpk61vC0ulSdZy',
text: 'This is a comment',
user: {
id: 'd5O4jTsLZHXmD1VJXwoE,
name: 'Charlie Martin',
avatar: 'https://...'
}
}
Now, you have everything you need to display the comment. If someone clicks the author of the comment, you can then take the id and use it to load the full profile of the commentor.
Data duplication is frowned upon in relational databases since they are built to handle these scenarios with foreign keys.
However, in NoSQL databases like firestore, data duplication is actually encouraged to simplify queries as well as reduce the amount of data sent over the network.
If you had loaded the full user
document for each comment, you'd likely be loading much more information about the user than is required to display the comment.

- 8,208
- 3
- 35
- 41
-
17Note that this with this approach you will get outdated data eg if user changes it's username (unless with each user update you will update all its occurrences) – Rafal Zawadzki Apr 15 '18 at 11:41
-
Correct. This is still the recommended approach by Google, though. It doesn't really matter, because the ID is still valid. For example, I might see in a comment thread a post by user "Joe", but when I click on that user, I see they have changed their name to "Bob". No big deal. – Charlie Martin Apr 28 '18 at 05:10
-
27Well, the size of the deal is bigger in my opinion ;) I clicked on "Joe" and suddenly I land on the profile of some Bob - and I become a Really Confused User: "are my fingers so fat? Is my vision blurry? I am pretty sure I wanted to see a profile of Joe and it's some Bob I've never heard of..." From my experience, average users have no idea about software and databases - they just know that the app is not working as they expect it to work – Rafal Zawadzki May 15 '18 at 05:16
-
15That would actually be pretty weird now that I think about it. In that case, I would write a cloud function that updates all the duplicated data when the user record is updated – Charlie Martin May 15 '18 at 05:39
-
2@CharlieMartin I think this is the correct approach. I think it's better and less costly to occassionally update user records in other places rather than reading the doc multiple times – ihor.eth May 12 '20 at 18:16
-
So the trade off is: better performance but more bugs -I can see why folks prefer to dedupe. – jprio Jul 25 '23 at 04:53
-
@jprio Yeah this tradeoff manifests often in NoSQL. After all, it's main goal is performance and scalability. You have to trade something. I will say I've been doing this for quite awhile and I don't have bugs per se, but you may occasionally have a temporary cache invalidation issue AKA stale state – Charlie Martin Jul 25 '23 at 18:46
Adding to ChrisRich response. If the desired field is an array of references you can use the following code:
import { get, set } from 'lodash'
const getReference = async documentReference => {
const res = await documentReference.get()
const data = res.data()
if (data && documentReference.id) {
data.uid = documentReference.id
}
return data
}
export default async (document, paths = []) => Promise.all(
paths.map(async path => {
const documentField = get(document, path)
if (documentField.constructor.name === 'Array') {
for (let i = 0; i < documentField.length; i++) {
const documentReference = documentField[i];
if (!documentReference || !documentReference.path) {
return console.warn(
`Error hydrating documentReference for path "${path}": Not found or invalid reference`
)
}
const result = await getReference(documentReference)
documentField[i] = result;
}
}
else {
const documentReference = documentField;
if (!documentReference || !documentReference.path) {
return console.warn(
`Error hydrating documentReference for path "${path}": Not found or invalid reference`
)
}
const result = await getReference(documentReference)
set(document, path, result)
}
})
)

- 21
- 3
FirebaseFirestore db = FirebaseFirestore.getInstance();
db.collection("Stock").whereEqualTo("user", userName).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
@Override
public void onComplete(@NonNull Task<QuerySnapshot> task) {
if (task.isComplete()) {
Log.d(TAG, "onComplete stock : "+ task.getResult().getDocuments().toString());
List<DocumentSnapshot> list = task.getResult().getDocuments();
if (list != null && list.size()>0) {
for (DocumentSnapshot doc: list) {
StockData stockData = new StockData();
stockData.setCatergory(doc.get("catergory").toString());
stockData.setDate(doc.get("date").toString());
stockData.setUser(doc.getString("date_time"));
stockData.setUser(doc.getString("user"));
stockData.setSale_price(doc.getDouble("sale_price"));
}
}
}
}
});

- 2,917
- 23
- 46
- 68

- 75
- 6
I create a solution for this, at least work for me!
Imagine u have 2 collections: users and friends and Users has one document: User1 and Friends has one document too: Friend1.
So User1 has a reference field with this text: Friends/Friend1.
U can get all Users and for each one u can build a map like this:
[
{
"User1":"Friend1"
}
{
"Another User":"Another Friend"
}
]

- 165
- 1
- 5