'Best practice way to update a parent document field that is computed from sub collection documents in google cloud firestore
Here's an abbreviated version of the data model for my firestore collections:
// /posts/{postId}
interface Post {
id: string;
lastCommentedAt: Timestamp | null
}
// /posts/{postId}/comments/{commentId}
interface Comment {
id: string;
createdAt: Timestamp;
}
So I have a collection of posts, and within each post is a subcollection of comments.
I want to do the following:
- When a comment is created, update the
lastCommentedAtfield of the parent Post document with the comment'screatedAtvalue - When a comment is deleted, the parent Post's
lastCommentedAtfield may no longer be valid so we need to get all of the Post's comments and get the most recent comment to updatelastCommentedAt.
I see a couple ways to do this:
- In my client code, I can do the above logic inside of functions like
createComment(post, comment), anddeleteComment(post, comment)- Especially in the case of deleting it seems like it is not ideal to require the client to fetch all comments for the post and iterate through them just to delete one
- Would I need to use transactions for this since someone could be deleting a comment at the same time someone was creating a new one?
- In my cloud functions I could create triggers on
/posts/{postId}/comments/{commentId}for create and delete and do this logic on the backend- Is there risk of race conditions here as well? Again maybe I should use a transaction?
The use case for this lastCommentedAt field is that I want to be able to query for posts and sort them by the ones that have recent comments.
Edit: possible implementation for deleteComment using a batched write. Is it actually safe to do a query for documents before the writes though?
async function deleteComment(post, comment) {
const batch = writeBatch(firestore);
const postRef = doc("posts", post.id);
const commentRef = doc("posts", post.id, "comments", comment.id);
const commentsCollection = collection("posts", post.id, "comments");
const recentCommentSnapshot = await getDocs(
query(
commentsCollection,
where("id", "!=", comment.id),
orderBy("createdAt", "desc"),
limit(1)
)
);
let lastCommentedAt = null;
if (recentCommentSnapshot.docs.length > 0) {
lastCommentedAt = recentCommentSnapshot.docs[0].data().createdAt;
}
batch.delete(commentRef);
batch.update(postRef, { lastCommentedAt });
await batch.commit();
}
Solution 1:[1]
For adding comments you can use batched writes to ensure the comment is added and the parent document is update with current timestamp.
const batch = db.batch();
const postDocRef = db.collection('posts').doc('postId');
const commentRef = postDocRef.collection("comments").doc();
batch.update(postDocRef, { lastCommentedAt: FieldValue.serverTimestamp() });
batch.set(commentRef, { createdAt: FieldValue.serverTimestamp() });
await batch.commit();
When deleting comments you might have to query the latest comment using createdAt field and then update parent document.
const getLastComment = (postId: string) => {
const postRef = db.collection('posts').doc(postId);
return await postRef.orderBy('createdAt', 'desc').limit(1).get();
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
