Firebase报错:Every document read in a transaction must also be written 及任务交易开发咨询
刚遇到这个报错我也头大过,先给你拆解清楚问题根源,再结合你的Airtasker类应用场景给出具体解决方案:
报错原因直击
Firebase事务的核心规则是:只要你在事务里读取了某个文档,就必须在同一个事务中对该文档执行写入操作。这是因为事务的设计是为了保证读取-修改-写入的原子性,如果只读不写,Firebase无法确保你依赖的数据在事务执行期间没有被其他客户端篡改,所以会抛出这个错误。
结合你的场景,大概率是你在事务中读取了tasks或者某个transactions文档,但没有对这个读取的文档做任何写入操作,直接去创建新的transaction文档,触发了规则校验。
针对你的业务需求的解决方案
你的需求是:用户批准出价时,检查对应task是否有transaction(支持多个承接者,即一个task可以有多个transaction),没有就新增状态为ongoing的transaction。下面分两种场景给出最优方案:
场景1:需要确保task存在才创建transaction(避免无效交易)
如果必须验证task存在,又要避免报错,我们可以在事务中读取task文档的同时,对它做一个微小的更新(比如维护一个交易计数字段),满足“读必写”的规则:
import { getFirestore, runTransaction, doc, setDoc, updateDoc } from "firebase/firestore"; const db = getFirestore(); async function createTransactionForApprovedBid(taskId, bidId) { try { await runTransaction(db, async (transaction) => { // 1. 读取目标task文档(必须在事务中写入它) const taskRef = doc(db, "tasks", taskId); const taskDoc = await transaction.get(taskRef); if (!taskDoc.exists()) { throw new Error("对应的任务不存在,无法创建交易"); } // 2. 生成新的transaction文档(自动ID,支持多个承接者) const newTransactionRef = doc(db, "transactions"); transaction.set(newTransactionRef, { taskId, bidId, status: "ongoing", createdAt: new Date() }); // 3. 更新task文档(关键!满足读必写规则) const currentTransactionCount = taskDoc.data().transactionCount || 0; transaction.update(taskRef, { transactionCount: currentTransactionCount + 1, updatedAt: new Date() }); }); console.log("交易记录创建成功"); } catch (err) { console.error("交易创建失败:", err.message); } }
场景2:无需在代码中校验task存在(用安全规则兜底)
如果你的业务允许通过Firebase安全规则来限制只有存在的task才能创建transaction,那可以完全避开事务,直接创建transaction文档,既简单又不会触发报错:
第一步:编写Firebase安全规则
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // 只有当对应的task存在时,才能创建transaction match /transactions/{transactionId} { allow create: if exists(/databases/$(database)/documents/tasks/$(request.resource.data.taskId)); } } }
第二步:直接创建transaction文档
import { getFirestore, doc, setDoc } from "firebase/firestore"; const db = getFirestore(); async function createTransactionForApprovedBid(taskId, bidId) { try { const newTransactionRef = doc(db, "transactions"); await setDoc(newTransactionRef, { taskId, bidId, status: "ongoing", createdAt: new Date() }); console.log("交易记录创建成功"); } catch (err) { console.error("交易创建失败:", err.message); } }
这种方案更轻量,适合支持多个承接者的场景——每个被批准的出价都对应独立的transaction文档,不需要检查已有交易。
额外优化建议
因为你支持多个承接者完成同一任务,建议在transactions文档中关联对应的bidId,这样可以清晰跟踪每个交易对应的出价,后续处理任务完成、打款等逻辑时更方便。
内容的提问来源于stack exchange,提问作者nrion

