Reputation: 5217
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
}
}
Upvotes: 1
Views: 2311
Reputation: 26171
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)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.
Upvotes: 2
Reputation: 83058
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
}
}
Upvotes: 1