Firebase 云功能 - 在 OnUpdate 云触发器中更新不同的对象

Firebase cloud functions - update a different object within OnUpdate cloud trigger

假设有一个用户集合,每个用户都与帐户相关联,这些帐户保存在一个单独的集合中。每个账户都有一个余额,该余额会通过一些外部方式(例如下面的 http 触发器)定期更新。我需要能够查询用户所有帐户的总余额。

我添加了 onUpdate 触发器,每次帐户更改时都会调用它并相应地更新总数。但是,似乎存在一些竞争条件,例如当两个账户大约同时更新时:在为第一个账户调用 onUpdate 并更新总余额后,在为第二个账户调用 onUpdate 时仍然没有更新。我猜我需要以某种方式使用 "transaction" 进行簿记,但不确定如何使用。

 const data = {
    'users/XXX': {
      email: "a@b.com",
      balance: 0
    },
    "accounts/YYY": {
      title: "Acc1",
      userID: "XXX"
      balance: 0
    },
    "accounts/ZZZ": {
      title: "Acc2",
      userID: "XXX"
      balance: 0
    }
  };

exports.updateAccounts = functions.https.onRequest((request, response) => {
  admin.firestore().collection('accounts').get().then((accounts) => {
    accounts.forEach((account) => {
      return admin.firestore().collection('accounts').doc(account.id).update({balance: 
WHATEVER});    
    })
 response.send("Done");
});

exports.updateAccount = functions.firestore
    .document('accounts/{accountID}')
    .onUpdate((change, context) => {
      const userID = change.after.data().userID;
      admin.firestore().doc("users/"+userID).get().then((user) => {
        const new_balance = change.after.data().balance;
        const old_balance = change.before.data().balance;
        var user_balance = user.data().balance + new_balance - old_balance;
        admin.firestore().doc("users/"+userID).update({balance: user_balance});
      });
    });

通过查看您的代码,我们可以看到其中的几个部分可能会导致不正确的结果。如果不彻底测试和重现您的问题,就不可能 100% 确定纠正它们将完全解决您的问题,但这很可能是问题的原因。

HTTP 云函数:

通过 forEach() 循环,您将调用多个异步操作(update() 方法),但您无需等待所有这些异步操作完成后再发回响应。你应该这样做,使用 Promise.all() 等待所有异步方法完成后再发送响应:

exports.updateAccounts = functions.https.onRequest((request, response) => {

  const promises = [];
  admin.firestore().collection('accounts').get()
  .then(accounts => {
      accounts.forEach((account) => {
        promises.push(admin.firestore().collection('accounts').doc(account.id).update({balance: WHATEVER}));
      return Promise.all(promises);
  })
  .then(() => {
      response.send("Done");
  })
  .catch(error => {....});
});

onUpdate后台触发云函数

您需要正确 return Promises 链,以便在 Cloud Function 完成时向平台指示。以下应该可以解决问题:

exports.updateAccount = functions.firestore
    .document('accounts/{accountID}')
    .onUpdate((change, context) => {

      const userID = change.after.data().userID;

      return admin.firestore().doc("users/"+userID).get()  //Note the return here. (Note that in the HTTP Cloud Function we don't need it! see the link to the video series below)
      .then(user => {
        const new_balance = change.after.data().balance;
        const old_balance = change.before.data().balance;
        var user_balance = user.data().balance + new_balance - old_balance;
        return admin.firestore().doc("users/"+userID).update({balance: user_balance});  //Note the return here.
      });
});

我建议您观看 Firebase 视频系列中关于 "JavaScript Promises" 的 3 个视频:https://firebase.google.com/docs/functions/video-series/。他们解释了上面更正的所有要点。


乍一看,如果您在 updateAccounts Cloud Function 中修改共享相同 user 的几个 account 文档,您确实需要实施用户交易中的余额更新,因为 updateAccount Cloud Function 的多个实例可能会并行触发。关于交易的文档是 here.

更新: 您可以在 updateAccounts 云函数(未测试)中实现如下交易:

exports.updateAccount = functions.firestore
.document('accounts/{accountID}')
.onUpdate((change, context) => {

    const userID = change.after.data().userID;

    const userRef = admin.firestore().doc("users/" + userID);

    return admin.firestore().runTransaction(transaction => {
        // This code may get re-run multiple times if there are conflicts.
        return transaction.get(userRef).then(userDoc => {
            if (!userDoc.exists) {
                throw "Document does not exist!";
            }

            const new_balance = change.after.data().balance;
            const old_balance = change.before.data().balance;
            var user_balance = userDoc.data().balance + new_balance - old_balance;

            transaction.update(userRef, {balance: user_balance});
        });
    }).catch(error => {
        console.log("Transaction failed: ", error);
        return null;
    });   

});

除了@Renaud Tarnec covered in 之外,您可能还需要考虑以下方法:

批量写入

在您的 updateAccounts 函数中,您同时写入了许多数据,如果其中任何一个失败,您最终可能会得到一个包含正确更新数据和已更新数据的混合数据库更新失败。

要解决此问题,您可以使用批量写入以原子方式写入数据,其中所有新数据都已成功更新,或者 none 数据已写入,使数据库处于已知状态。

exports.updateAccounts = functions.https.onRequest((request, response) => {
  const db = admin.firestore();
  db.collection('accounts')
    .get()
    .then((qsAccounts) => { // qs -> QuerySnapshot
      const batch = db.batch();
      qsAccounts.forEach((accountSnap) => {
        batch.update(accountSnap.ref, {balance: WHATEVER});
      })
      return batch.commit();
    })
    .then(() => response.send("Done"))
    .catch((err) => {
      console.log("Error whilst updating balances via HTTP Request:", err);
      response.status(500).send("Error: " + err.message)
    });
});

拆分计数器

与其在您的文档中存储单个 "balance",不如将每个帐户的余额存储在用户的文档中(根据您的尝试)。

"users/someUser": {
  ...,
  "balances": {
    "accountId1": 10,
    "accountId4": -20,
    "accountId23": 5
  }
}

如果您需要累积余额,只需在客户端将它们相加即可。如果您需要删除余额,只需在用户文档中删除它的条目即可。

exports.updateAccount = functions.firestore
  .document('accounts/{accountID}')
  .onUpdate((change, context) => {
    const db = admin.firestore();
    const accountID = context.params.accountID;
    const newData = change.after.data();

    const accountBalance = newData.balance;
    const userID = newData.userID;
    return db.doc("users/"+userID)
      .get()
      .then((userSnap) => {
        return db.doc("users/"+userID).update({["balances." + accountID]: accountBalance});
      })
      .then(() => console.log(`Successfully updated account #${accountID} balance for user #${userID}`))
      .catch((err) => {
        console.log(`Error whilst updating account #${accountID} balance for user #${userID}`, err);
        throw err;
      });
  });