【问题标题】:Firebase cloud function onUpdate is triggered but doesn't execute as wantedFirebase 云功能 onUpdate 已触发,但未按要求执行
【发布时间】:2021-08-16 14:46:39
【问题描述】:

我目前正在使用 React 前端和 Firebase 后端制作一个 Web 应用程序。它是一个本地健身房的应用程序,由两部分组成:

  • 在当地健身房训练的人的客户端应用程序
  • 适用于当地健身房教练的教练申请

当地的健身房为公司提供课程。因此,一家公司进行订阅,公司的员工可以在当地的健身房训练并使用客户端应用程序。重要的是要跟踪公司员工的个人进度以及整个进度(公司 x 的所有员工一起减掉的公斤数)。

在 Firestore 集合“用户”中,每个用户文档都有字段 bodyweight。每当培训师在对特定客户进行身体评估后填写进度表时,客户用户文档中的体重字段就会更新为新的体重。

在 Firestore 中还有另一个集合“公司”,每个公司都有一个文档。我的目标是将公司员工损失的总公斤数放入该特定文件中。因此,每次培训师更新员工的体重时,都需要更新公司文档。我制作了一个云功能来监听用户文档的更新。功能如下:

exports.updateCompanyProgress = functions.firestore
  .document("users/{userID}")
  .onUpdate((change, context) => {

  const previousData = change.before.data();
  const data = change.after.data();

  if (previousData === data) {
    return null;
  }

  const companyRef = admin.firestore.doc(`/companies/${data.company}`);
  const newWeight = data.bodyweight;
  const oldWeight = previousData.bodyweight;
  const lostWeight = oldWeight > newWeight;
  const difference = diff(newWeight, oldWeight);
  const currentWeightLost = companyRef.data().weightLostByAllEmployees;

  if (!newWeight || difference === 0 || !oldWeight) {
    return null;
  } else {
    const newCompanyWeightLoss = calcNewCWL(
      currentWeightLost,
      difference,
      lostWeight
    );
    companyRef.update({ weightLostByAllEmployees: newCompanyWeightLoss });
  }
});

上面的云函数里面有两个简单的函数:

const diff = (a, b) => (a > b ? a - b : b - a);

const calcNewCWL = (currentWeightLost, difference, lostWeight) => {
  if (!lostWeight) {
    return currentWeightLost - difference;
  }
  return currentWeightLost + difference;
};

我已将云功能部署到 Firebase 以对其进行测试,但无法使其正常工作。该函数在用户文档更新时触发,但它不会使用新的 weightLostByAllEmployees 值更新公司文档。这是我第一次使用 Firebase 云功能,这么大的变化是某种菜鸟的错误。

【问题讨论】:

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


    【解决方案1】:

    您当前的解决方案中存在一些我们可以解决的错误。

    总是false 相等检查

    您使用以下相等性检查来确定数据是否未更改:

    if (previousData === data) {
      return null;
    }
    

    这将始终是 false,因为 change.before.data()change.after.data() 返回的对象将始终是不同的实例,即使它们包含相同的数据。

    公司变更从不处理

    虽然这可能是罕见的,甚至是不可能的事件,但如果用户的公司发生了变化,您应该从原公司的总数中删除他们的权重并将其添加到新公司。

    类似地,当员工离开公司或删除其帐户时,您应该从 onDelete 处理程序的总数中删除他们的权重。

    处理浮点和

    如果您不知道,浮点运算有一些小怪癖。以总和 0.1 + 0.2 为例,对于人类,答案是 0.3,但对于 JavaScript 和许多语言,答案是 0.30000000000000004。请参阅this question & thread 了解更多信息。

    与其将体重作为浮点数存储在数据库中,不如考虑将其存储为整数。由于权重通常不是整数(例如9.81kg),您应该存储该值乘以 100(对于 2 个有效数字),然后将其四舍五入到最接近的整数。然后当你显示它时,你要么将它除以 100,要么用适当的十进制符号拼接。

    const v = 1201;
    console.log(v/100); // -> 12.01
    
    const vString = String(v);
    console.log(vString.slice(0,-2) + "." + vString.slice(-2) + "kg"); // -> "12.01kg"
    

    因此对于总和,0.1 + 0.2,您可以将其放大到 10 + 20,结果为 30

    console.log(0.1 + 0.2); // -> 0.30000000000000004
    console.log((0.1*100 + 0.2*100)/100); // -> 0.3
    

    但这种策略本身并不是万无一失的,因为某些乘法运算仍然会出现这些错误,例如0.14*100 = 14.0000000000000020.29*100 = 28.999999999999996。为了清除这些,我们将乘积四舍五入。

    console.log(0.01 + 0.14); // -> 0.15000000000000002
    console.log((0.01*100 + 0.14*100)/100); // -> 0.15000000000000002
    console.log((Math.round(0.01*100) + Math.round(0.14*100))/100) // -> 0.15
    

    您可以使用以下方法进行比较:

    const arr = Array.from({length: 100}).map((_,i)=>i/100);
    
    console.table(arr.map((a) => arr.map((b) => a + b)));
    console.table(arr.map((a) => arr.map((b) => (a*100 + b*100)/100)));
    console.table(arr.map((a) => arr.map((b) => (Math.round(a*100) + Math.round(b*100))/100)));
    

    因此我们可以得到这些辅助函数:

    function sumFloats(a,b) {
      return (Math.round(a * 100) + Math.round(b * 100)) / 100;
    }
    
    function sumFloatsForStorage(a,b) {
      return (Math.round(a * 100) + Math.round(b * 100));
    }
    

    以这种方式处理权重的主要好处是,您现在可以使用FieldValue#increment() 而不是完整的事务来快捷地更新值。在同一家公司的两个用户发生更新冲突的极少数情况下,您可以重试增量或回退到完整事务。

    低效的数据解析

    在您当前的代码中,您在前后状态中使用.data() 来获取函数所需的数据。但是,因为您要拉取用户的整个文档,所以您最终会解析文档中的所有字段,而不仅仅是您需要的 - bodyweightcompany 字段。您可以使用DocumentSnapshot#get(fieldName) 执行此操作。

    const afterData = change.after.data(); // parses everything - username, email, etc.
    const { bodyweight, company } = afterData;
    

    对比:

    const bodyweight = change.after.get("bodyweight"); // parses only "bodyweight"
    const company = change.after.get("company"); // parses only "company"
    

    冗余数学

    由于某种原因,您正在计算权重之间差异的绝对值,将差异符号存储为布尔值,然后将它们一起使用以将更改应用回总重量损失。

    以下几行:

    const previousData = change.before.data();
    const data = change.after.data();
    
    const newWeight = data.bodyweight;
    const oldWeight = previousData.bodyweight;
    const lostWeight = oldWeight > newWeight;
    const difference = diff(newWeight, oldWeight);
    const currentWeightLost = companyRef.data().weightLostByAllEmployees;
    
    const calcNewCWL = (currentWeightLost, difference, lostWeight) => {
      if (!lostWeight) {
        return currentWeightLost - difference;
      }
      return currentWeightLost + difference;
    };
    
    const newWeightLost = calcNewCWL(currentWeightLost, difference, lostWeight);
    

    可以替换为:

    const newWeight = change.after.get("bodyweight");
    const oldWeight = change.before.get("bodyweight");
    const deltaWeight = newWeight - oldWeight;
    const currentWeightLost = companyRef.get("weightLostByAllEmployees") || 0;
    
    const newWeightLost = currentWeightLost + deltaWeight;
    

    将所有内容组合在一起

    exports.updateCompanyProgress = functions.firestore
      .document("users/{userID}")
      .onUpdate(async (change, context) => {
    
      // "bodyweight" is the weight scaled up by 100
      // i.e. "9.81kg" is stored as 981
      const oldHundWeight = change.before.get("bodyweight") || 0;
      const newHundWeight = change.after.get("bodyweight") || 0;
      
      const oldCompany = change.before.get("company");
      const newCompany = change.after.get("company");
      
      const db = admin.firestore();
      
      if (oldCompany === newCompany) {
        // company unchanged
        const deltaHundWeight = newHundWeight - oldHundWeight;
        
        if (deltaHundWeight === 0) {
          return null; // no action needed
        }
        
        const companyRef = db.doc(`/companies/${newCompany}`);
        
        await companyRef.update({
          weightLostByAllEmployees: admin.firestore.FieldValue.increment(deltaHundWeight)
        });
      } else {
        // company was changed
        
        const batch = db.batch();
        
        const oldCompanyRef = db.doc(`/companies/${oldCompany}`);
        const newCompanyRef = db.doc(`/companies/${newCompany}`);
        
        // remove weight from old company
        batch.update(oldCompanyRef, {
          weightLostByAllEmployees: admin.firestore.FieldValue.increment(-oldHundWeight)
        });
        
        // add weight to new company
        batch.update(newCompanyRef, {
          weightLostByAllEmployees: admin.firestore.FieldValue.increment(newHundWeight)
        });
        
        // apply changes
        await db.batch();
      }
    });
    

    带有事务回退

    在发生写入冲突的极少数情况下,此变体会退回到传统事务以重新尝试更改。

    /**
     * Increments weightLostByAllEmployees in all documents atomically
     * using a transaction.
     *
     * `arrayOfCompanyRefToDeltaWeightPairs` is an array of company-increment pairs.
     */
    function transactionIncrementWeightLostByAllEmployees(db, arrayOfCompanyRefToDeltaWeightPairs) {
      return db.runTransaction((transaction) => {
        // get all needed documents, then add the update for each to the transaction
        return Promise
          .all( 
            arrayOfCompanyRefToDeltaWeightPairs
              .map(([companyRef, deltaWeight]) => {
                return transaction.get(companyRef)
                  .then((companyDocSnapshot) => [companyRef, deltaWeight, companyDocSnapshot])
              })
          )
          .then((arrayOfRefWeightSnapshotGroups) => {
            arrayOfRefWeightSnapshotGroups.forEach(([companyRef, deltaWeight, companyDocSnapshot]) => {
              const currentValue = companyDocSnapshot.get("weightLostByAllEmployees") || 0;
              transaction.update(companyRef, {
                weightLostByAllEmployees: currentValue + deltaWeight
              })
            });
          });
      });
    }
    
    exports.updateCompanyProgress = functions.firestore
      .document("users/{userID}")
      .onUpdate(async (change, context) => {
    
      // "bodyweight" is the weight scaled up by 100
      // i.e. "9.81kg" is stored as 981
      const oldHundWeight = change.before.get("bodyweight") || 0;
      const newHundWeight = change.after.get("bodyweight") || 0;
      
      const oldCompany = change.before.get("company");
      const newCompany = change.after.get("company");
      
      const db = admin.firestore();
      
      if (oldCompany === newCompany) {
        // company unchanged
        const deltaHundWeight = newHundWeight - oldHundWeight;
        
        if (deltaHundWeight === 0) {
          return null; // no action needed
        }
        
        const companyRef = db.doc(`/companies/${newCompany}`);
        
        await companyRef
          .update({
            weightLostByAllEmployees: admin.firestore.FieldValue.increment(deltaHundWeight)
          })
          .catch((error) => {
            // if an unexpected error, just rethrow it
            if (error.code !== "resource-exhausted")
              throw error;
          
            // encountered write conflict, fall back to transaction
            return transactionIncrementWeightLostByAllEmployees(db, [
              [companyRef, deltaHundWeight]
            ]);
          });
      } else {
        // company was changed
        
        const batch = db.batch();
        
        const oldCompanyRef = db.doc(`/companies/${oldCompany}`);
        const newCompanyRef = db.doc(`/companies/${newCompany}`);
        
        // remove weight from old company
        batch.update(oldCompanyRef, {
          weightLostByAllEmployees: admin.firestore.FieldValue.increment(-oldHundWeight)
        });
        
        // add weight to new company
        batch.update(newCompanyRef, {
          weightLostByAllEmployees: admin.firestore.FieldValue.increment(newHundWeight)
        });
        
        // apply changes
        await db.batch()
          .catch((error) => {
            // if an unexpected error, just rethrow it
            if (error.code !== "resource-exhausted")
              throw error;
          
            // encountered write conflict, fall back to transaction
            return transactionIncrementWeightLostByAllEmployees(db, [
              [oldCompanyRef, -oldHundWeight],
              [newCompanyRef, newHundWeight]
            ]);
          });
      }
    });
    

    【讨论】:

    • 感谢您提供我所见过的最详细的答案。我真的从中学到了一些东西,现在云功能可以完美运行。
    【解决方案2】:

    你的云函数有几点需要适应:

    • 使用admin.firestore() 而不是admin.firestore
    • 您无法通过companyRef.data()获取公司文档的数据。您必须调用异步get() 方法。
    • 更新公司文档时使用Transaction返回此交易返回的承诺(有关此关键方面的更多详细信息,请参阅here)。

    所以下面的代码应该可以解决问题。

    请注意,由于我们使用了 Transaction,因此我们实际上并没有实现上面第二个要点的建议。我们改用transaction.get(companyRef)

    exports.updateCompanyProgress = functions.firestore
        .document("users/{userID}")
        .onUpdate((change, context) => {
    
            const previousData = change.before.data();
            const data = change.after.data();
    
            if (previousData === data) {
                return null;
            }
    
            // You should do admin.firestore() instead of admin.firestore
            const companyRef = admin.firestore().doc(`/companies/${data.company}`);
    
    
            const newWeight = data.bodyweight;
            const oldWeight = previousData.bodyweight;
            const lostWeight = oldWeight > newWeight;
            const difference = diff(newWeight, oldWeight);
    
            if (!newWeight || difference === 0 || !oldWeight) {
                return null;
            } else {
                return admin.firestore().runTransaction((transaction) => {
    
                    return transaction.get(companyRef).then((compDoc) => {
                        if (!compDoc.exists) {
                            throw "Document does not exist!";
                        }
                        const currentWeightLost = compDoc.data().weightLostByAllEmployees;
                        const newCompanyWeightLoss = calcNewCWL(
                            currentWeightLost,
                            difference,
                            lostWeight
                        );
    
                        transaction.update(companyRef, { weightLostByAllEmployees: newCompanyWeightLoss });
                    });
                })
            }
        });
    

    【讨论】:

      猜你喜欢
      • 2020-10-20
      • 2021-11-27
      • 2021-04-08
      • 1970-01-01
      • 2021-10-26
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多