Облачные функции Firebase - обновить другой объект в облачном триггере OnUpdate - PullRequest
0 голосов
/ 09 марта 2020

Предположим, что существует коллекция пользователей, и каждый пользователь связан с учетными записями, которые хранятся в отдельной коллекции. Для каждой учетной записи есть баланс, который периодически обновляется некоторыми внешними средствами (например, триггер 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});
      });
    });

Ответы [ 2 ]

1 голос
/ 09 марта 2020

В дополнение к тому, что @ Renaud Tarne c включено в их ответ , вы можете также рассмотреть следующие подходы:

Пакетная запись

В вашей функции 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;
      });
  });
1 голос
/ 09 марта 2020

Посмотрев на ваш код, мы увидим несколько его частей, которые могут привести к неверным результатам. Без тщательного тестирования и воспроизведения вашей проблемы невозможно на 100% быть уверенным, что их исправление полностью решит вашу проблему, но, скорее всего, это является причиной проблем.

Облачная функция HTTP:

С forEach() l oop вы вызываете несколько асинхронных операций (метод 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 Фоновая функция облачного запуска

Там вам нужно правильно вернуть цепочку Promises, чтобы указывать платформе, когда облачная функция завершена. Следующее должно сделать трюк:

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

Я бы посоветовал вам посмотреть 3 видео о "JavaScript Promises" из серии видеороликов Firebase: https://firebase.google.com/docs/functions/video-series/ , Они объясняют все ключевые моменты, которые были исправлены выше.


На первый взгляд кажется, что если вы измените в updateAccounts облачной функции несколько account документов, которые имеют один и тот же user вам действительно потребуется реализовать обновление баланса пользователя в транзакции, поскольку несколько экземпляров updateAccount Cloud Function могут запускаться параллельно. Do c для Транзакций - здесь .

Обновление: Вы можете реализовать транзакцию следующим образом в 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;
    });   

});
...