Firebase云函数-更新OnUpdate云触发器中的不同对象

Firebase云函数-更新OnUpdate云触发器中的不同对象,firebase,promise,google-cloud-firestore,async-await,transactions,Firebase,Promise,Google Cloud Firestore,Async Await,Transactions,假设有一个用户集合,每个用户都与帐户关联,这些帐户保存在单独的集合中。每个账户都有一个余额,该余额通过一些外部手段定期更新,例如下面的http触发器。我需要能够查询用户所有帐户的总余额 我添加了onUpdate触发器,每次帐户更改时都会调用它,并相应地更新总数。但是,似乎存在一些竞争条件,例如,当两个帐户同时更新时:在为第一个帐户调用onUpdate并更新总余额后,当为第二个帐户调用onUpdate时,它仍然没有更新。我猜我需要以某种方式使用事务进行记账,但不确定如何使用 const data

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

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

 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方法,但在发送回响应之前,您不会等待所有这些异步操作都完成。在发送响应之前,您应该按如下操作,使用等待所有异步方法完成:

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后台触发云功能 在这里,您需要正确返回承诺链,以便向平台指示云功能何时完成。下面应该可以做到这一点:

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承诺的3个视频:。他们解释了上面更正的所有关键点

乍一看,如果您在updateAccounts云函数中修改共享同一用户的多个帐户文档,您确实需要在事务中实现用户余额更新,因为updateAccount云函数的多个实例可能会并行触发。关于交易的单据是

更新: 可以在未测试的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;
    });   

});

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

HTTP云功能: 在forEach循环中,您调用了几个异步操作update方法,但在发送回响应之前,您不会等待所有这些异步操作都完成。在发送响应之前,您应该按如下操作,使用等待所有异步方法完成:

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后台触发云功能 在这里,您需要正确返回承诺链,以便向平台指示云功能何时完成。下面应该可以做到这一点:

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承诺的3个视频:。他们解释了上面更正的所有关键点

乍一看,如果您在updateAccounts云函数中修改共享同一用户的多个帐户文档,您确实需要在事务中实现用户余额更新,因为updateAccount云函数的多个实例可能会并行触发。关于交易的单据是

更新: 可以在未测试的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;
    });   

});

除了所涵盖的内容外,您还可以考虑以下方法:

批写入 在updateAccounts函数中,您一次写入多个数据,如果其中任何一个失败,您可能会得到一个数据库,其中包含正确更新的数据和未能更新的数据

要解决这个问题,您可以使用批处理写入以原子方式写入数据,其中所有新数据都已成功更新,或者没有任何数据被写入,而数据库处于已知状态

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)
    });
});
拆分计数器 与其在文档中存储单个余额,不如根据您试图在用户文档中存储每个帐户的余额所做的操作来选择

"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;
      });
  });
如果您需要累计余额,只需在客户机上将它们相加即可。如果需要删除余额,只需在用户文档中删除其条目即可

"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;
      });
  });

除了所涵盖的内容外,您还可以考虑以下方法:

批写入 在updateAccounts函数中,您一次写入多个数据,如果其中任何一个失败,您可能会得到一个数据库,其中包含正确更新的数据和可用的数据 d未能更新

要解决这个问题,您可以使用批处理写入以原子方式写入数据,其中所有新数据都已成功更新,或者没有任何数据被写入,而数据库处于已知状态

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)
    });
});
拆分计数器 与其在文档中存储单个余额,不如根据您试图在用户文档中存储每个帐户的余额所做的操作来选择

"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;
      });
  });
如果您需要累计余额,只需在客户机上将它们相加即可。如果需要删除余额,只需在用户文档中删除其条目即可

"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;
      });
  });

谢谢你的回复。我不会等待HTTP云函数中的结果,因为我并不真正关心它——它只是测试底层对象更改的一种方法。但在生产中,我将有一些触发器,可以同时更新多个帐户,而不必在单个事务中,因为它们是独立的。我想问题仍然存在——我如何在onUpdate函数中调用您提到的事务?你能举个例子吗?非常感谢-这非常有帮助,效果非常好。请仅在transaction.update行中将userDoc更改为userRef,我会将其标记为答案:-感谢您的回复。我不会等待HTTP云函数中的结果,因为我并不真正关心它——它只是测试底层对象更改的一种方法。但在生产中,我将有一些触发器,可以同时更新多个帐户,而不必在单个事务中,因为它们是独立的。我想问题仍然存在——我如何在onUpdate函数中调用您提到的事务?你能举个例子吗?非常感谢-这非常有帮助,效果非常好。请仅在transaction.update行中将userDoc更改为userRef,我会将其标记为答案:-谢谢@samthecodingman。我认为这里不需要批量写入,因为这些余额是独立的,不需要同时更新所有余额。事实上,有不同类型的帐户,它们可能由不同的触发器分别更新。拆分计数器并在客户机上汇总是我将考虑的一个选项。我仍然对如何解决最初的问题感兴趣,即使我最终没有在这里使用它-@shaimo虽然可能不需要批量写入,但这是一种预防措施,可以确保数据库的数据完整性。批写入的目的是确保应用0%或100%的更新。使用Promise.all可能会导致您的数据在0-100%之间被写入,如果不修改代码来跟踪,您将不知道什么是成功的,什么是失败的。跟进只是为了确保我没有遗漏什么-因为不同的帐户是独立的,即使只有部分数据得到更新,数据也应该是一致的。这就是为什么我认为我不需要对它进行批处理。在任何情况下,在生产代码中,我都会习惯于在需要时进行一致的更新。@shaimo我已经用一个示例编写了一个更好的解释,可能会更容易理解。把它包括在这里的答案中是没有意义的。根据你在最近的评论中所说的,选项2很可能是一个不错的选择。谢谢@samthecodingman。我认为这里不需要批量写入,因为这些余额是独立的,不需要同时更新所有余额。事实上,有不同类型的帐户,它们可能由不同的触发器分别更新。拆分计数器并在客户机上汇总是我将考虑的一个选项。我仍然对如何解决最初的问题感兴趣,即使我最终没有在这里使用它-@shaimo虽然可能不需要批量写入,但这是一种预防措施,可以确保数据库的数据完整性。批写入的目的是确保应用0%或100%的更新。使用Promise.all可能会导致您的数据在0-100%之间被写入,如果不修改代码来跟踪,您将不知道什么是成功的,什么是失败的。跟进只是为了确保我没有遗漏什么-因为不同的帐户是独立的,即使只有部分数据得到更新,数据也应该是一致的。这就是为什么我认为我不需要对它进行批处理。在任何情况下,在生产代码中,我都会习惯于在需要时进行一致的更新。@shaimo我已经用一个示例编写了一个更好的解释,可能会更容易理解。把它包括在这里的答案中是没有意义的。根据你在最近的评论中所说的,选择2很可能是一条出路。