与其使用普通事务,不如使用Batched Write 更有效,因为您只是在写入数据而不关心当前文档状态。与普通事务一样,批量写入会对您的数据库进行原子(全有或全无)更改。
重要的是,您应该支持一次处理删除超过 500 个文档,这是 Firestore 事务的限制。可以使用:
async function deleteDocs(db, docRefs) {
return Promise.all(
Array
.from({length: Math.ceil(docRefs.length/500)})
.map((_, i) => docRefs.slice(500*i, 500*(i+1))) // <-- splits docRefs into arrays with at most 500 entries
.map((docRefsInChunk) => {
// create a new batch that will delete all refs in docRefsInChunk
const batch = db.batch();
docRefsInChunk.forEach(ref => batch.delete(ref));
// commit the result, but sink the error to not error-out `Promise.all`
return batch.commit()
.then(
() => ({ success: true }),
(error) => ({ success: false, error, docRefs: docRefsInChunk })
);
})
);
}
接下来,我们需要处理失败的删除。如果您未能删除一个或多个文档,我们应该在您的数据库中的某个位置创建一个文档,以便我们稍后手动或使用另一个 Cloud Function 重新尝试删除。
async function queueDeleteRetry(db, { errors, docRefs }) {
// Create a new "Retry deletion" document reference
const retryDocRef = db.collection("_server/retry/delete").doc();
// Collate list of targets for the deletion by pulling their paths
const docPaths = docRefs.map(ref => ref.path);
// Set the data on the retry document
return retryDocRef
.set({
attempts: 0,
causes: errors.map(({ code, message, name, stack }) => ({
code: code || null,
message,
name,
stack: stack || null
})),
targets: docPaths,
timestamp: Date.now()
})
.then(
() => ({ queued: true, path: retryDocRef.path }),
(error) => ({ queued: false, error, targets: docPaths })
);
}
将这些应用到您的代码中:
exports.deleteOrderAndInformation = functions.https.onCall(
async (data, context) => {
const { admin: adminToken, pa } = context.auth.token;
// TODO: Check assumption
const { orderId } = data;
if (!adminToken && !pa)
throw new functions.https.HttpsError(
"permission-denied",
"Insufficent permissions to delete order"
);
if (!orderId)
throw new functions.https.HttpsError(
"invalid-argument",
"Missing or falsy property \"orderId\""
);
try {
const db = admin.firestore();
// collections that may contain documents to be cleaned up
const batchCols = [
"extrusionBatches",
"printingBatches",
"laminationBatches",
"slittingBatches",
"conversionBatches",
"filmBatches",
"filmEvents"
];
// find all the documents references that are linked to this order
const queriedDocRefArrayPromises = batchCols.map(col => {
return db
.collection(col)
.where("orderId", "==", orderId)
.get()
.then(() => qSnapshot.docs.map(doc => doc.ref)); // <- pull out DocumentReferences here for performance
});
// get all the document references as one array
const docRefs = (await Promise.all(queriedDocRefArrayPromises))
.flat();
// include the original order reference
const orderRef = db.doc(`filmOrders/${orderId}`);
docRefs.push(orderRef);
// attempt deletion
const deleteBatchResults = await deleteDocs(db, docRefs);
const failedResults = deleteBatchResults
.reduce((info, result) => {
if (!result.success) {
info.errors.push(result.error);
info.docRefs.push(...result.docRefs);
}
return info;
}, { errors: [], docRefs: [] });
if (failedResults.errors.length === 0) {
// done! return response to finish!
return {
success: true,
message: `Order #${orderId} and all linked documents were deleted successfully!`
}
}
// some/all deletions failed
// Create a document containing all the failed deletions
const queueResult = await queueDeleteRetry(db, failedResults);
// If it couldn't create a document, log the same info
if (!queueResult.queued) {
functions.logger.error({
message: "Failed to delete AND queue deletion retry of documents",
targets: queueResult.targets,
"logging.googleapis.com/labels": {
customError: "failed-deletion" // <- custom label to search for them
}
});
}
// calculate counts
const totalCount = docRefs.length;
const failedCount = failedResults.docRefs.length;
// return info about failure to finish
return {
success: false,
message: totalCount === failedCount
? `Failed to delete all documents related to order #${orderId}!`
: `Failed to delete ${failedCount}/${totalCount} documents related to order #${orderId}!`,
detail: {
orderId,
counts: {
failed: failedCount
total: totalCount
},
retryQueued: queueResult.queued
? { path: queueResult.path } // <- this can be used on the client to track the retry status/intervene manually
: false
}
}
} catch (error) {
throw new functions.https.HttpsError("cancelled", error.toString());
}
}
);
以上代码返回以下响应之一:
{
"success": true,
"message": "Order #8dZZohPZ8aoTv1eB7N2F and all linked documents were deleted successfully"
}
{
"success": false,
"message": "Failed to delete all documents related to order #8dZZohPZ8aoTv1eB7N2F!",
"detail": {
"orderId": "8dZZohPZ8aoTv1eB7N2F",
"counts": {
"failed": 8,
"total": 8
},
"retryQueued": {
"path": "_server/retry/delete/Li8U4ZV6k6sDvIN3pju5"
}
}
}
{
"success": false,
"message": "Failed to delete 42/542 documents related to order #8dZZohPZ8aoTv1eB7N2F!",
"detail": {
"orderId": "8dZZohPZ8aoTv1eB7N2F",
"counts": {
"failed": 42,
"total": 542
},
"retryQueued": false // <- when retry couldn't be created and log was used
}
}