我做的 Firestore 交易是否正确?

Am I doing Firestore Transactions correct?

我已经按照与交易相关的 Firestore 文档进行操作,我想我已经正确地对它们进行了排序,但是在测试中我注意到我的文档有时没有得到正确更新的问题。有可能在很短的时间内将文档的多个版本提交给函数,但我只对保留最新版本感兴趣。

我的大体逻辑是这样的:
New/Updated 文档发送到云端功能 检查文档是否已存在于 Firestore 中,如果不存在,请添加它。

如果确实存在,请检查它是否比 firestore 中的实例“更新”,如果是,请更新它。

否则什么都不做

这是我的函数中试图实现此目的的代码...如果这是 correct/best 实现此目的的方法,我希望得到一些反馈:

const ocsFlight = req.body;
  const procFlight = processOcsFlightEvent(ocsFlight);
  try {
    const ocsFlightRef = db.collection(collection).doc(procFlight.fltId);
    const originalFlight = await ocsFlightRef.get();

    if (!originalFlight.exists) {
      const response = await ocsFlightRef.set(procFlight);
      console.log("Record Added: ", JSON.stringify(procFlight));
      res.status(201).json(response); // 201 - Created
      return;
    }

    await db.runTransaction(async (t) => {
      const doc = await t.get(ocsFlightRef);
      const flightDoc = doc.data();

      if (flightDoc.recordModified <= procFlight.recordModified) {
        t.update(ocsFlightRef, procFlight);
        console.log("Record Updated: ", JSON.stringify(procFlight));
        res.status(200).json("Record Updated");
        return;
      }

      console.log("Record isn't newer, nothing changed.");
      console.log("Record:", JSON.stringify("Same Flight:", JSON.stringify(procFlight)));
      res.status(200).json("Record isn't newer, nothing done.");
      return;
    });
  } catch (error) {
    console.log("Error:", JSON.stringify(error));
    res.status(500).json(error.message);
  }

虫虫

首先,您相信 req.body 的值是正确的形状。如果您还没有类型断言来反映 processOcsFlightEvent/collection/someFlightId 的安全规则,您应该添加它们。这很重要,因为来自 Admin SDK 的任何数据库操作都会绕过您的安全规则。

下一个错误是在事务内向您的函数发送响应。一旦您将响应发送回客户端,您的函数就会被标记为不活动 - 资源受到严重限制,任何网络请求都可能无法完成或崩溃。由于如果检测到数据库冲突,事务可能会重试几次,因此您应确保仅在事务正确完成后才响应客户端。

您使用 set 将新航班写入 Firestore,这可能会在处理交易时出现问题,因为 set 操作将取消该位置的所有待处理交易。如果两个函数实例争夺同一个航班 ID,这将导致将错误数据写入数据库的问题。

在您当前的代码中,您将 return 对客户端的 ocsFlightRef.set() 操作结果作为 HTTP 201 Created 响应的 body。由于 DocumentReference#set() 的结果是 WriteResult object,如果你想将它 return 发送给客户端,你需要正确地序列化它,即便如此,我认为它不会很有用,因为您似乎没有将它用于其他响应类型。相反,HTTP 201 Created 响应通常包括资源被写入的位置 Location header 而没有 body,但这里我们将传递 [=60] 中的路径=].如果您开始使用多个数据库实例,包括相关数据库也可能会有用。

修复

获得预期结果的正确方法是在一个事务中执行整个读取->检查->写入过程,并且仅在事务完成后才响应客户端。

所以我们可以向客户端发送适当的响应,我们可以使用交易的 return 值将数据传递出去。我们将传递所做更改的类型 ("created" | "updated" | "aborted") 和数据库中存储内容的 recordModified 值。我们将 return 这些连同资源的 path 和适当的 message.

如果出现错误,我们将 return 一条消息向用户显示 message,错误的 Firebase 错误代码(如果可用)或一般消息显示为 error 属性.

// if not using express to wrangle requests, assert the correct method
if (req.method !== "POST") {
  console.log(`Denied ${req.method} request`);
  res.status(405) // 405 - Method Not Allowed
    .set("Allow", "POST")
    .end(); 
  return;
}

const ocsFlight = req.body;
try {
  // process AND type check `ocsFlight`
  const procFlight = processOcsFlightEvent(ocsFlight);

  const ocsFlightRef = db.collection(collection).doc(procFlight.fltId);

  const { changeType, recordModified } = await db.runTransaction(async (t) => {
    const flightDoc = await t.get(ocsFlightRef);

    if (!flightDoc.exists) {
      t.set(ocsFlightRef, procFlight);
      return {
        changeType: "created",
        recordModified: procFlight.recordModified
      };
    }

    // only parse the field we need rather than everything
    const storedRecordModified = flightDoc.get('recordModified');

    if (storedRecordModified <= procFlight.recordModified) {
      t.update(ocsFlightRef, procFlight);
      return {
        changeType: "updated",
        recordModified: procFlight.recordModified
      };
    }

    return {
      changeType: "aborted",
      recordModified: storedRecordModified
    };
  });

  switch (changeType) {
    case "updated":
      console.log("Record updated: ", JSON.stringify(procFlight));
      res.status(200).json({ // 200 - OK
        path: ocsFlightRef.path,
        message: "Updated",
        recordModified,
        changeType
      });
      return;
    case "created":
      console.log("Record added: ", JSON.stringify(procFlight));
      res.status(201).json({ // 201 - Created
        path: ocsFlightRef.path,
        message: "Created",
        recordModified,
        changeType
      });
      return;
    case "aborted":
      console.log("Outdated record discarded: ", JSON.stringify(procFlight));
      res.status(200).json({ // 200 - OK
        path: ocsFlightRef.path,
        message: "Record isn't newer, nothing done.",
        recordModified,
        changeType
      });
      return;
    default:
      throw new Error("Unexpected value for 'changeType': " + changeType);
  }
} catch (error) {
  console.log("Error:", JSON.stringify(error));
  res.status(500) // 500 - Internal Server Error
    .json({
      message: "Something went wrong",
      // if available, prefer a Firebase error code
      error: error.code || error.message 
    });
}

参考资料