1

I am learning about Firestore's batch writes method and it looks really neat. Almost async-like! However, I am needing some help figuring out how to run a batch statement when doing a forEach() on a query.

My use case is that when a user deletes a post, I need to also "clean up" and update/delete other items associated with that post. That could be all bookmarks users have created for this post, likes, etc.

Here is an example of a deletePost function. How do you run a batch statement on the bookmarksQuery and usersAnswerQuery queries?

async deletePost(post) {
  const response = confirm('Delete this post?')
  const batch = this.$fire.firestore.batch()
  if (response === true && this.userProfile.uid === this.post.uid) {
    try {
      const postRef = this.$fire.firestore
        .collection(`users/${post.uid}/posts`)
        .doc(this.post.id)

      const answerRef = this.$fire.firestore
        .collection('answers')
        .doc(this.post.commentIdWithAnswer)

      const usersAnswerQuery = await this.$fire.firestore
        .collectionGroup('answers')
        .where('id', '==', this.post.commentIdWithAnswer)
        .get()

      const bookmarksQuery = await this.$fire.firestore
        .collectionGroup('bookmarks')
        .where('id', '==', this.post.id)
        .get()

      batch.update(postRef, {
        published: false,
        deleted: true,
        updatedAt: this.$fireModule.firestore.FieldValue.serverTimestamp()
      })

      bookmarksQuery.forEach((doc) => doc.ref.delete()) //<---- how to add this to batch?

      usersAnswerQuery.forEach((doc) => doc.ref.delete()) //<---- how to add this to batch?

      batch.delete(answerRef)

      await batch.commit()
      // To do: delete all user 'likes' associated with this post
      
      
      alert('Post successfully deleted!')
    } catch (error) {
      console.error('error deleting post.', error)
    }
  } else {
    return null
  }
}
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
redshift
  • 4,815
  • 13
  • 75
  • 138

2 Answers2

2

To add a document deletion to the batch, you would use WriteBatch#delete() like you have done for answerRef using:

// prepare the batch
const batch = firebase.firestore().batch();

// add each doc's deletion to the batch
docs.forEach((doc) => batch.delete(doc.ref));

// commit the changes
await batch.commit();

While the above approach works fine, a batched write has a limit of 500 operations. As you will likely hit this limit on popular posts while tidying up bookmarks, answers and likes, we need to handle this case. We can achieve this by tracking the number of operations you've added into the batch and create a new batch each time you reach the limit.

// prepare the batch
let currentBatch = firebase.firestore().batch();
let currentBatchSize = 0;
const batches = [ currentBatch ];

// add each doc's deletion to the batch
docs.forEach((doc) => {
  // when batch is too large, start a new one
  if (++currentBatchSize >= 500) {
    currentBatch = firebase.firestore.batch();
    batches.push(currentBatch);
    currentBatchSize = 1;
  }

  // add operation to batch
  currentBatch.delete(doc.ref);
})

// commit the changes
await Promise.all(batches.map(batch => batch.commit()));

Other things I've noticed in your current code:

  • deletePost has an inconsistent return type of Promise<void | null> - consider returning a Promise<boolean> (to indicate success, because you are handling errors in your function)
  • You ask for user confirmation before checking whether the post can actually be deleted by the current user - you should check first
  • Silently fails to delete another user's post, instead of showing an error (this should also be enforced by security rules)
  • Silently fails to delete the post, without showing a message to the user
  • You have a large if block followed by a tiny else block, you should flip it so you can "fail-fast" and not need to indent most of the code.

Applying the solution plus these other changes gives:

async deletePost(post) {
  if (this.userProfile.uid !== this.post.uid) {
    alert("You can't delete another user's post.");
    return false; // denied
  }

  const response = confirm('Delete this post?')
  
  if (!response)
    return false; // cancelled
  
  try {
    const postRef = this.$fire.firestore
      .collection(`users/${post.uid}/posts`)
      .doc(this.post.id)

    const answerRef = this.$fire.firestore
      .collection('answers')
      .doc(this.post.commentIdWithAnswer)

    const usersAnswerQuery = await this.$fire.firestore
      .collectionGroup('answers')
      .where('id', '==', this.post.commentIdWithAnswer)
      .get()

    const bookmarksQuery = await this.$fire.firestore
      .collectionGroup('bookmarks')
      .where('id', '==', this.post.id)
      .get()
      
    let currentBatch = this.$fire.firestore.batch();
    const batches = [currentBatch];    

    currentBatch.update(postRef, {
      published: false,
      deleted: true,
      updatedAt: this.$fireModule.firestore.FieldValue.serverTimestamp()
    });
    
    currentBatch.delete(answerRef);
    
    let currentBatchSize = 2;

    const addDocDeletionToBatch = (doc) => {
      if (++currentBatchSize >= 500) {
        currentBatch = this.$fire.firestore.batch();
        batches.push(currentBatch);
        currentBatchSize = 1;
      }
    
      currentBatch.delete(doc.ref);
    }

    bookmarksQuery.forEach(addDocDeletionToBatch)
    usersAnswerQuery.forEach(addDocDeletionToBatch)

    // TODO: delete all user 'likes' associated with this post

    // commit changes
    await Promise.all(batches.map(batch => batch.commit()));
    
    alert('Post successfully deleted!')
    return true;
  } catch (error) {
    console.error('error deleting post.', error)
    alert('Failed to delete post!');
    return false;
  }
}

Note: If you use the standard comments // TODO and // FIXME, you can make use of many tools that recognise and highlight these comments.

samthecodingman
  • 23,122
  • 4
  • 30
  • 54
  • Wow! Fantastic...let me study it in detail. Thank you so much for your contribution...I'm more of a designer rather than programmer, so your answer was very helpful in my coding journey. – redshift Jun 15 '21 at 18:52
1

Do as follows. Do not forget the 500 docs limit for a batched write (which includes deletions).

async deletePost(post) {
  const response = confirm('Delete this post?')
  const batch = this.$fire.firestore.batch()
  if (response === true && this.userProfile.uid === this.post.uid) {
    try {
      // ...

      batch.update(postRef, {
        published: false,
        deleted: true,
        updatedAt: this.$fireModule.firestore.FieldValue.serverTimestamp()
      })

      bookmarksQuery.forEach((doc) => batch.delete(doc.ref))

      usersAnswerQuery.forEach((doc) => batch.delete(doc.ref))

      batch.delete(answerRef)

      await batch.commit()
      // To do: delete all user 'likes' associated with this post
      
      
      alert('Post successfully deleted!')
    } catch (error) {
      console.error('error deleting post.', error)
    }
  } else {
    return null
  }
}
Renaud Tarnec
  • 79,263
  • 10
  • 95
  • 121
  • Wow, that's right, I forgot to add the `ref` part when I tried something similar. Thanks again for the helpful answer, Renaud! Really excited to work with Firestore now. – redshift Jun 15 '21 at 18:42