316

I'm just exploring the new Firebase Firestore and it contains a data type called reference. It is not clear to me what this does.

  • Is it like foreign key?
  • Can it be used to point to a collection that is located somewhere else?
  • If reference is an actual reference, can I use it for queries? For example can I have a reference that points directly to the user, instead of storing the userId in a text field? And can I use this user reference for querying?
3
  • 33
    I think this video from firebase team breaks it down for you: youtube.com/watch?v=Elg2zDVIcLo (watch from 4:36) Commented Jan 24, 2019 at 5:03
  • 26
    youtu.be/Elg2zDVIcLo?t=276 Commented Jan 17, 2020 at 20:50
  • 1
    I don't like to nest collections in firebase for multiple reasons. If for some reason you had another root level collection that you need to drill all the way down on a sibling root collection; let's say 4 levels to get to a document. This is made a lot easier by using refs and just using db.doc('some_saved_ref') vs matching all the ids out again... from the other root collection. Commented Jun 22, 2021 at 16:34

9 Answers 9

207

Adding below what worked for me using references in Firestore.

As the other answers say, it's like a foreign key. The reference attribute doesn't return the data of the reference doc though. For example, I have a list of products, with a userRef reference as one of the attributes on the product. Getting the list of products, gives me the reference of the user that created that product. But it doesn't give me the details of the user in that reference. I've used other back end as a services with pointers before that have a "populate: true" flag that gives the user details back instead of just the reference id of the user, which would be great to have here (hopefully a future improvement).

Below is some example code that I used to set the reference as well as get the collection of products list then get the user details from the user reference id given.

Set a reference on a collection:

let data = {
  name: 'productName',
  size: 'medium',
  userRef: db.doc('users/' + firebase.auth().currentUser.uid)
};
db.collection('products').add(data);

Get a collection (products) and all references on each document (user details):

db.collection('products').get()
    .then(res => {
      vm.mainListItems = [];
      res.forEach(doc => {
        let newItem = doc.data();
        newItem.id = doc.id;
        if (newItem.userRef) {
          newItem.userRef.get()
          .then(res => { 
            newItem.userData = res.data() 
            vm.mainListItems.push(newItem);
          })
          .catch(err => console.error(err));
        } else {
          vm.mainListItems.push(newItem);  
        }
        
      });
    })
    .catch(err => { console.error(err) });
Sign up to request clarification or add additional context in comments.

22 Comments

Thanks for sharing! I think there is a typo in the first line of Get part and it should be db.collection('products').get(). Have you tried getting user directly? I'm guessing newItem.userRef.get() should work instead of db.collection("users").doc(newItem.userRef.id).get()
First of all thank you for the example. I hope they will add a "populate: true" for the future. Otherwise saving a reference is somewhat pointless. The same could have been done by simply saving the uid and reference via it.
Thanks for the example! But what is the point of storing the reference type if there is no "populate" kind of option when we query the document? If there is a populate option that anyone knows of, please let me know.
So in fact it's not like a foreign key. To me it does basically nothing - what's the point of having reference if we can't use it as a true foreign key should work?
So the only advantage of a reference over a string is, that you can call get() on the reference directly. Not very useful yet. Hope they add an option to automatically populate references with the corresponding objects!
|
140

References are very much like foreign keys.

The currently released SDKs cannot store references to other projects. Within a project, references can point to any other document in any other collection.

You can use references in queries like any other value: for filtering, ordering, and for paging (startAt/startAfter).

Unlike foreign keys in a SQL database, references are not useful for performing joins in a single query. You can use them for dependent lookups (which seem join like), but be careful because each hop will result in another round trip to the server.

13 Comments

Please, can you share possible use cases? Is it possible to query fields in that reference? E.g. I have a friends collection listing all my friends (friends/myId). Then, I reference this document in the friends field of another document (group/groupId). I'd like to display only my friends who are in that group, doing something like this: where('friends.myId', '==', true).
Btw, it might be useful to update the docs to include an example of adding a reference type.
I can't find any info about this? This will change my whole database structure, I need to know more ...
do you have an example (preferably in swift) on how to query using reference? right now, I can do it by storing the raw uid as string, but that's doesn't seems right.
I'm needing to change all my reference types to strings because the query's will always fail with a reference type. I literally cannot find anything about how to query by reference type :( if anyone finds out how to query by reference types let me know...
|
35

For those looking for a Javascript solution to querying by reference - the concept is that, you need to use a 'document reference' object in the query statement

teamDbRef = db.collection('teams').doc('CnbasS9cZQ2SfvGY2r3b'); /* CnbasS9cZQ2SfvGY2r3b being the collection ID */
//
//
db.collection("squad").where('team', '==', teamDbRef).get().then((querySnapshot) => {
  //
}).catch(function(error) {
  //
});

(Kudos to the answer here: https://stackoverflow.com/a/53141199/1487867)

Comments

12

A lot of answers mentioned it is just a reference to another document but does not return data for that reference but we can use it to fetch data separately.

Here is an example of how you could use it in the firebase JavaScript SDK 9, modular version.

let's assume your firestore have a collection called products and it contains the following document.

{
  name: 'productName',
  size: 'medium',
  userRef: 'user/dfjalskerijfs'
}

here users have a reference to a document in the users collection. we can use the following code segment to get the product and then retrieve the user from the reference.

import { collection, getDocs, getDoc, query, where } from "firebase/firestore";
import { db } from "./main"; // firestore db object

let productsWithUser = []
const querySnaphot = await getDocs(collection(db, 'products'));
querySnapshot.forEach(async (doc) => {
  let newItem = {id: doc.id, ...doc.data()};
  if(newItem.userRef) {
    let userData = await getDoc(newItem.userRef);
    if(userData.exists()) {
      newItem.userData = {userID: userData.id, ...userData.data()}
    }
    productwithUser.push(newItem);
  } else {
    productwithUser.push(newItem);
  }
});

here collection, getDocs, getDoc, query, where are firestore related modules we can use to get data whenever necessary. we use user reference returned from the products document directly to fetch the user document for that reference using the following code,

let userData = await getDoc(newItem.userRef);

to read more on how to use modular ver SDK refer to official documentation to learn more.

1 Comment

Just be aware though every request to the ref will be counted towards read
11

According to the #AskFirebase https://youtu.be/Elg2zDVIcLo?t=276 the primary use-case for now is a link in Firebase console UI

2 Comments

Which still does not work - at least I cannot spot it in the UI :)
Why would you build a data type for the primary purpose of link in Firebase console UI?
6

If you don't use Reference data type, you need to update every document.

For example, you have 2 collections "categories" and "products" and you stored the category name "Fruits" in categories to every document of "Apple" and "Lemon" in products as shown below. But, if you update the category name "Fruits" in categories, you also need to update the category name "Fruits" in every document of "Apple" and "Lemon" in products:

collection | document | field

categories > 67f60ad3 > name: "Fruits"
collection | document | field

  products > 32d410a7 > name: "Apple", category: "Fruits"
             58d16c57 > name: "Lemon", category: "Fruits"

But, if you store the reference of "Fruits" in categories to every document of "Apple" and "Lemon" in products, you don't need to update every document of "Apple" and "Lemon" when you update the category name "Fruits" in categories:

collection | document | field

  products > 32d410a7 > name: "Apple", category: categories/67f60ad3
             58d16c57 > name: "Lemon", category: categories/67f60ad3

This is the goodness of Reference data type.

1 Comment

The discussion is not so much about storing the static name vs. a 'Foreign-Key-Like' id; but rather the benefit of using a doc reference vs. just using the doc ID as a string.
1

Update 3/31/24

I wrote a blog post explaining all the uses of a Firestore Reference Type.


Original Post


Automatic JOINS:

DOC

expandRef<T>(obs: Observable<T>, fields: any[] = []): Observable<T> {
  return obs.pipe(
    switchMap((doc: any) => doc ? combineLatest(
      (fields.length === 0 ? Object.keys(doc).filter(
        (k: any) => {
          const p = doc[k] instanceof DocumentReference;
          if (p) fields.push(k);
          return p;
        }
      ) : fields).map((f: any) => docData<any>(doc[f]))
    ).pipe(
      map((r: any) => fields.reduce(
        (prev: any, curr: any) =>
          ({ ...prev, [curr]: r.shift() })
        , doc)
      )
    ) : of(doc))
  );
}

COLLECTION

expandRefs<T>(
  obs: Observable<T[]>,
  fields: any[] = []
): Observable<T[]> {
  return obs.pipe(
    switchMap((col: any[]) =>
      col.length !== 0 ? combineLatest(col.map((doc: any) =>
        (fields.length === 0 ? Object.keys(doc).filter(
          (k: any) => {
            const p = doc[k] instanceof DocumentReference;
            if (p) fields.push(k);
            return p;
          }
        ) : fields).map((f: any) => docData<any>(doc[f]))
      ).reduce((acc: any, val: any) => [].concat(acc, val)))
        .pipe(
          map((h: any) =>
            col.map((doc2: any) =>
              fields.reduce(
                (prev: any, curr: any) =>
                  ({ ...prev, [curr]: h.shift() })
                , doc2
              )
            )
          )
        ) : of(col)
    )
  );
}

Simply put this function around your observable and it will automatically expand all reference data types providing automatic joins.

Usage

this.posts = expandRefs(
  collectionData(
    query(
      collection(this.afs, 'posts'),
      where('published', '==', true),
      orderBy(fieldSort)
    ), { idField: 'id' }
  )
);

Note: You can also now input the fields you want to expand as a second argument in an array.

['imageDoc', 'authorDoc']

This will increase the speed!

Add .pipe(take(1)).toPromise(); at the end for a promise version!

See here for more info. Works in Firebase 8 or 9!

Simple!

J

5 Comments

Please could you explain your code a bit more? I'm finding it difficult to understand what the pipes and map-reducers are doing. Thanks!
I agree with Dylan. This code is super hard to read and that makes it difficult to receive any benefit from it. Can you try commenting what some parts of it are doing. It also looks like you have nested map and reduce that would add significant amounts of runtime complexity.
this is not an update, this is someone trying to build a third party library on top of firebase's mistake...
@AbirTaheer - It only goes through every field with map if you do not input the specific fields you want to expand. @-RafaelLima - Agreed this a problem with Firestore, but I'm just trying to share the code that I wrote and find useful. Even if Firestore had joins, it would sill charge you to read the other documents. I can make comments on the code itself, but it really just goes through every field that is of reference type and expands it, or only the fields you input.
This answer is complete garbage. Just throwing a bunch of unreadable code using combineLatest, switchMap, map and pipe together and saying "this is what it's used for" helps nobody.
0

Belatedly, there are two advantages from this blog:

enter image description here

if I expect that I'll want to order restaurant reviews by rating, or publish date, or most upvotes, I can do that within a reviews subcollection without needing a composite index. In the larger top level collection, I'd need to create a separate composite index for each one of those, and I also have a limit of 200 composite indexes.

I wouldn't have 200 composite indices but there are some constraints.

Also, from a security rules standpoint, it's fairly common to restrict child documents based on some data that exists in their parent, and that's significantly easier to do when you have data set up in subcollections.

One example would be restricting to insert a child collection if the user doesn't have the privilege in the parent's field.

Comments

0

2022 UPDATE

let coursesArray = [];
const coursesCollection = async () => {
    const queryCourse = query(
        collection(db, "course"),
        where("status", "==", "active")
    )
    onSnapshot(queryCourse, (querySnapshot) => {
        querySnapshot.forEach(async (courseDoc) => {

            if (courseDoc.data().userId) {
                const userRef = courseDoc.data().userId;
                getDoc(userRef)
                    .then((res) => {
                        console.log(res.data());
                    })
            }
            coursesArray.push(courseDoc.data());
        });
        setCourses(coursesArray);
    });
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.