【问题标题】:Delete documents using a transaction and query - Firestore使用事务和查询删除文档 - Firestore
【发布时间】:2021-08-02 08:07:37
【问题描述】:

我有一个filmOrders 集合,当在那里创建文档时,它会在其他 7 个集合中创建文档。我想从filmOrders 中删除一个文档,并从其他集合中删除具有filmOrders 中该ID 的所有文档。

为此,filmOrders 中的文档 ID 存储在这些文档中,然后我可以查询其他 7 个集合中的 ID 并通过事务删除它们。

问题是,我不确定我这样做是否正确。我读过this question,它说您不能在事务中使用查询引用。但下面我查询数据库,获取文档,然后使用事务删除文档。

这是正确的方法吗?可以使用来自filmOrders 的 ID 添加更多文档,因此我不确定在此事务运行时这些文档是否会被删除

"use strict";
const functions = require("firebase-functions");
const admin = require("firebase-admin");

exports.deleteOrderAndInformation = functions.https.onCall(
  async (data, context) => {
    const { admin: adminToken, pa } = context.auth.token;

    if (!adminToken && !pa)
      throw new functions.https.HttpsError(
        "permission-denied",
        "Insufficent permissions to delete order"
      );

    try {
      const db = admin.firestore();
      await db.runTransaction(async t => {
        //we have to identify the docs to use in the transaction ahead of time
        //https://stackoverflow.com/questions/50071700/can-we-not-query-collections-inside-transactions
        const { orderId } = data;
        const orderRef = db.doc(`filmOrders/${orderId}`);

        //get all batches and events that had that orderID in it
        const batchCols = [
          "extrusionBatches",
          "printingBatches",
          "laminationBatches",
          "slittingBatches",
          "conversionBatches",
          "filmBatches",
          "filmEvents"
        ];

        const queriesPromises = batchCols.map(col => {
          return db
            .collection(col)
            .where("orderId", "==", orderId)
            .get();
        });

        //this will return all the query snapshots
        const queriesResolved = await Promise.all(queriesPromises);

        //extract the document refs
        const docRefs = queriesResolved.map(qSnapshot => {
          return qSnapshot.docs.map(doc => doc.ref);
        });

        const docRefsFlat = docRefs.flat();

        const tDocs = await t.getAll(...docRefsFlat, orderRef);
        const tDelete = tDocs.map(doc => {
          return t.delete(doc.ref);
        });

        await Promise.all(tDelete);
      });
    } catch (error) {
      throw new functions.https.HttpsError("cancelled", error.toString());
    }
  }
);

【问题讨论】:

    标签: node.js firebase google-cloud-firestore google-cloud-functions


    【解决方案1】:

    这可能在filmOrders 集合的删除触发器上做得更好。无论哪种方式,在开始交易之前进行查询。云函数应该返回 runTransaction 的结果,并且不应该等待不返回承诺的删除...

    async function getOtherRefs(orderId) {
      // your query of all the collections, flattening the results
    }
    
    exports.deleteOrderAndInformation = functions.https.onCall(async (orderId, context) => {
      const otherRefs = await getOtherRefs(orderId);
      const allRefs = [...otherRefs, db.doc(`filmOrders/${orderId}`)];
      
      return db.runTransaction(transaction => {
        const docs = await transaction.getAll(allRefs);
        docs.forEach(doc => transaction.delete(doc.ref));
      })
    });
    

    【讨论】:

    • 我想过onDelete,但我想知道如果交易失败会发生什么?我将不再有 orderId 可用于重新运行它(假设整个 onDelete 现在已经出错了)
    【解决方案2】:

    与其使用普通事务,不如使用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
      }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-03-21
      • 2019-02-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2020-10-28
      相关资源
      最近更新 更多