简体   繁体   中英

How can I avoid a race condition when updating a field that's dependent multiple documents in a subcollection in Google cloud firestore?

Here's a simplified representation of the data models for my firestore collections

// /posts/{postId}
interface Post {
  id: string; 
  lastCommentedAt: Timestamp | null
}

// /posts/{postId}/comments/{commentId}
interface Comment {
  id: string;
  createdAt: Timestamp;
}

So there's a collection of posts, and within each post is a subcollection of comments.

If a post has no comments, the lastCommentedAt field should be null. Otherwise, lastCommmentedAt should match the createdAt value for the most recent comment on that post. The utility of this field is to enable me to query for posts and sort them by ones that have recent comments.

I'm having trouble thinking through the scenario of deleting comments.

When a comment is deleted, the value of lastCommentedAt will become stale if the comment being deleted is the most recent comment on that post, and so will need to be updated. This is where I'm struggling to come up with a safe solution.

My first thought was to query for the most recent comment on the post that doesn't match the comment to be deleted, and then do a batched write where I delete the comment and update lastCommentedAt on the post. Example Javscript code using the Firebase web SDK here:

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();
}

However, I believe the code above would be vulnerable to a race condition if new comments are created after the query but before the writes, or if the recent comment was deleted after the query but before the writes.

So I think I need a transaction, but you can't query for documents in a transaction, so I'm not really sure where to go from here.

You could use Firestore Getting real-time updates which listens to changes between snapshots. Here's an example from the documentation:

import { collection, query, where, onSnapshot } from "firebase/firestore";

const q = query(collection(db, "cities"), where("state", "==", "CA"));
const unsubscribe = onSnapshot(q, (snapshot) => {
  snapshot.docChanges().forEach((change) => {
    if (change.type === "added") {
        console.log("New city: ", change.doc.data());
    }
    if (change.type === "modified") {
        console.log("Modified city: ", change.doc.data());
    }
    if (change.type === "removed") {
        console.log("Removed city: ", change.doc.data());
    }
  });
});

In here you can listen to any changes to your documents.


Check the code that I created which is triggered when the document is deleted from firestore and automatically update the posts.lastCommentedAt :

    var latestCreatedAt = null;
    const q = query(
        collection(db, "posts", "post.id", "comments")
    );
    const recentCommentQuery = query(
        collection(db, "posts", "post.id", "comments"),
        orderBy("createdAt", "desc"),
        limit(1)
    );
    const postsRef = query(collection(db, "posts"));
    const postRef = doc(db, "posts", "post.id"); 

    async function deletedComment (createdAt) {
        const querySnapshot = await getDocs(recentCommentQuery);

        querySnapshot.forEach((doc) => {
            latestCreatedAt = doc.data().createdAt.toDate();
        });

        await updateDoc(postRef, {
            lastCommentedAt: latestCreatedAt
        })
    }

    const unsubscribe = onSnapshot(q, (snapshot, querySnapshot) => {
        snapshot.docChanges().forEach((change) => {
            if (change.type === "removed") {
                deletedComment(change.doc.data().createdAt.toDate());
            }
        });
    });

You can even add a condition when adding or modifying documents that will also trigger to update the posts.lastCommentedAt .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM