This post series is indexed at NgateSystems.com. You'll find a super-useful keyword search facility there too.
Last reviewed: Nov '24
Introduction
Here are "boilerplate" templates for the most important variants of the Firestore Client API library. My suggestion is that you cut and paste them into your code as is then replace the word "my" in variable names with some suitable contraction of the name of the collection they target.
So, for example, "myCollRef" references for a collection called "Lecture_events" might be edited to "lecEvtsCollRef".
If you find these Firestore Client API templates useful for your browser-hosted work, you also want something similar for server-side code forced to use the Firestore Admin API.
I thought of writing a parallel "Firestore CRUD templates for the Admin API" version of this post, but as of November 2022, you can simply ask chatGPT to take a bunch of Client API code and convert it. Just submit a question like the following:
How would you rework the following Firestore Client API javascript code for the Firestore Admin API?
const myCollRef = collection(db, "myCollectionName");
const myDocRef = doc(myCollRef);
await setDoc(myDocRef, myDocData);
In response, you'll get a word-perfect Admin API version.
Creating documents
To create a document containing a myDocData object with an automatically-generated id:
let myDocData = .... create an object containing your data item properties .....
const myCollRef = collection(db, "myCollectionName");
const myDocRef = doc(myCollRef);
await setDoc(myDocRef, myDocData);
Note that, confusingly, Google Documentation on 'Adding Data' references an addDoc
function as an alternative to setDoc
. See the Postscript below for advice on why setDoc
is preferred.
In the code snippet above, the myDocRef=
statement is the point at which an auto-id is allocated. If you need to find the value that's been assigned, you'll find this at myDocRef.id
. Again, see the Postscript below for further information on this point.
To create a document with a data item as its identifier :
let myDocData = .... create a data object .....
let myDocumentIdentifier = .... create your identifier ....
const myDocRef = doc(db, "myCollectionName", myDocumentIdentifier)
await setDoc(myDocRef, myDocData);
Reading documents
To retrieve an individual document using its document id:
const myDocRef = doc(db, "myCollectionName", myDocId);
const myDoc = await getDoc(myDocRef);
if (myDoc.exists()) {
console.log("Document data:", myDoc.data());
}
To retrieve a selection of documents with selection and ordering criteria (example):
const myCollRef = collection(db, "myCollectionName");
const myQuery = query(myCollRef,
where("myField1Name", "==", myField1Value),
where("myField2Name", "==", myField2Value),
orderBy("myField2Name", "asc"));
const mySnapshot = await getDocs(myQuery);
mySnapshot.forEach((myDoc) => {
console.log(myDoc.id, " => ", myDoc.data());
});
Within a Snapshot's forEach
, the data for a document is available as myDoc.data()
, the document's docRef is myDoc.ref
and its docId as myDoc.id
. If you're just interested in determining the existence of document(s) that match the selection criteria, a useful trick is to check for non-zero mySnapshot.size
.
If you want to refer to individual documents in the snapshot
array, you'll find the data for the n'th entry at mySnapshot.docs[n].data()
and its id at mySnapshot.docs[n].id
Note that if you don't specify an orderBy
field, documents will be returned in ascending order of docId. Also that if you specify more than one where
field, you will need to create a (compound) index. The browser inspection tool will help you here - just follow the link embedded in the "index-needed" error message. Individual fields are indexed automatically in a Firestore database.
To retrieve all of the documents in a collection:
const myCollRef = collection(db, "myCollectionName");
const myQuery = query(myCollRef);
const mySnapshot = await getDocs(myQuery);
mySnapshot.forEach((myDoc) => {
console.log(myDoc.id, " >= ", myDoc.data());
});
Firestore comparison operators are "==", ">" , "<", "<=", ">=" and "!=", plus some interesting array membership operators.
To retrieve all of the documents in a hierarchy of collections and then do something:
You have to be careful when you want to perform a certain action after processing on a multi-level hierarchy of collections has concluded. If your code contains a number of nested foreach
statements, each containing an await
instruction, you can't rely on the individual awaits
to tell you when the whole set has finished. Each of these individual await
s occupies its own separate thread and these don't communicate directly with each other in any helpful way.
One way out of this problem is to use traditional for
loop on your snapshot
s rather than forEach
s. Here's an example targeting all the children in a sub-collection before performing an action
const myParentsCollRef = collection(db, "myParentCollectionName");
const myParentsQuery = query(myParentsCollRef);
const myParentsSnapshot = await getDocs(myParentsQuery);
for (let i = 0; i < myParentsSnapshot.size; i++) {
let myParentDocId = myParentsSnapshot.docs[i].data()
const myChildrenCollRef = collection(db, "myParentCollectionName", myParentDocId, "myChildrenCollectionName");
const myChildrenQuery = query(myChildrenCollRef);
const myChidrenSnapshot = await getDocs(myChildrenQuery);
for (let j = 0; j < myParentsSnapshot.size; j++) {
console.log(JSON.stringify(myChidrenSnapshot.docs[j].data()));
}
}
Here, you can rely on your await
s to be performed in strict sequence, and when you hit the end of the loop you know you can carry on confidently to perform your dependant action. But the performance hit created by this may be significant and so you might be interested in the following arrangement:
You can get a handle on the individual promise
s launched by the await
s in a forEach
loop by storing them in an array. You can then apply an await Promise.all
instruction to this array to find out when all its member promises are done. It's not possible to provide a simple template here to suit all circumstances, but the following is a "sketch" that illustrates the broad principles.
Here, a two-level hierarchy involving two separate collections (parents and children) are linked by a common parentsId field . The two collections are read into memory so that some sort of analysis can be performed on the aggregate. This can only be done when all the children have been read.
const aggregateArray =[]
const parentsCollRef = collection(db, "parents");
const parentsQuery = query(parentsCollRef);
const parentsSnapshot = await getDocs(parentsQuery);
const promisesArray = [];
parentsSnapshot.forEach((parentsDoc) => {
// for clarity, the nested awaits required to get the children for each parent are coded as an explicit function
promisesArray.push(fetchChildren((parentsDoc))
})
// and here's the function itself
async function fetchChildren(parentsDoc) {
const childrenCollRef = collection(db, "children");
const childrenQuery = query(childrenCollRef, where("parentsId", "==", parentsDoc.data().parentsId));
const childrenSnapshot = await getDocs(childrenQuery);
chidrenSnapshot.forEach((childrenDoc) => {//push parent and children data into the aggregate array
})
}
// and now you can perform your aggregate analysis.
await Promise.all(promisesArray);
Updating a document
Example - to change the value of the myField property in a document's myDocData content
const myDocRef = doc(db, 'myCollectionName', myDocId);
await setDoc(myDocRef, {myField: myFieldValue}, { merge: true });
Example - to replace the entire content of document myDocId with a new object containing only a myField property
const myDocRef = doc(db, 'myCollectionName', myDocId);
await setDoc(myDocRef, {myField: myFieldValue}, { merge: false });
You can apply changes to a number of fields simultaneously by replacing the {myField: myFieldValue}
bit in the above examples by an object containing the fields you want to change.
Deleting a document
const myDocRef = doc(db, 'myCollectionName', myDocId);
await deleteDoc(myDocRef);
CRUD operations within transactions
Inside a transaction, the patterns introduced above remain unchanged but the precise form of the setDoc commands are amended as follows:
Within the runTransaction(db, async (transaction) => { ... }).catch();
function:
- getDoc is replaced by transaction.get()
- setDoc is replaced by transaction.set()
- deleteDoc is replaced by transaction.delete()
Postscript
As mentioned above, Google provides
addDoc()
andupdateDoc()
functions for document creation and update in parallel withsetDoc()
. But this seems unnecessarily confusing when setDoc can perform both operations. Also, when it comes to transactions,addDoc()
can only be used for the creation of documents with auto ids. It seems simpler, in practice, just to use setDoc everywhere.You may have noticed that there's no
await
on thedoc(myCollRef)
call that creates a Firestore document identifier. This tells you that Firestore somehow manages to do this without actually visiting the collection and seeing what is already in use. If you're curious about how it manages this you might like to check out the discussion at StackOverflow.
If you have found this post interesting, you might find it useful to check out the index to this series
Google documentation references
Add data to Cloud Firestore : https://firebase.google.com/docs/firestore/manage-data/add-data
Read data with Cloud Firestore : https://firebase.google.com/docs/firestore/query-data/get-data
Delete data from Cloud Firestore : https://firebase.google.com/docs/firestore/manage-data/delete-data
SDK documentation can be found at:
Top comments (1)
Nice work