
本文详解在 prisma 中使用 `$transaction` 回调模式安全创建主从记录(如账户 + 开户余额交易),避免 id 引用错误,并对比嵌套写入等更简洁的替代方案。
在 Prisma v5 中,若需在创建 accounts 后立即基于其生成的 id 创建关联的 transactions(例如开户余额交易),不能直接在事务数组中通过 prisma.account.fields.id 引用未提交的 ID——该写法是无效的,fields 是元数据对象,不提供运行时值。正确的做法是改用 回调式事务(callback transaction),它支持顺序执行、变量捕获与错误回滚,语义清晰且类型安全。
✅ 推荐方案:使用回调式事务($transaction(async (tx) => {}))
const newAccount = await prisma.$transaction(async (tx) => { // 步骤 1:创建账户,获取返回的完整对象(含自动生成的 id) const account = await tx.account.create({ data: { accountCode: Number(accountCode), name: name?.trim() ?? '', type, description: description ?? '', balance: Number(balance), status, branchId, createdById: req.user.id, }, }); // 步骤 2:仅当余额 > 0 时,创建开户余额交易,直接引用 account.id if (Number(balance) > 0) { await tx.transaction.create({ data: { type: 'OPENING_BALANCE', amount: Number(balance), reference: uuidv4(), description: `Account opening balance for ${account.name} created by ${req.user.name}`, status: 'ACTIVE', branchId, accountId: account.id, // ✅ 正确:使用上一步返回的实际 ID createdById: req.user.id, }, }); } return account; // 可选:返回主记录便于后续使用 });
? 关键点说明: tx 是事务上下文实例,所有操作共享同一数据库会话和事务边界; account.id 是 promise 解析后的实际值(如 ‘acc_abc123’),可安全用于外键赋值; 整个回调内任意步骤失败,事务自动回滚,保证数据一致性。
⚡ 更简洁替代:嵌套写入(推荐用于强关联场景)
若 Transaction 模型已正确定义 accountId 为外键,且 Account 模型中配置了 transactions 关系字段(如 transactions: { type: ‘Transaction’, list: true, relationName: ‘accountTransactions’ }),则可省略显式事务,直接使用 嵌套写入(nested write):
// 方式一:从 Account 创建,同时创建 Transaction await prisma.account.create({ data: { accountCode: Number(accountCode), name: name?.trim() ?? '', type, description: description ?? '', balance: Number(balance), status, branchId, createdById: req.user.id, // 嵌套创建交易(仅当有余额) transactions: Number(balance) > 0 ? { create: { type: 'OPENING_BALANCE', amount: Number(balance), reference: uuidv4(), description: `Account opening balance for ${name?.trim()} created by ${req.user.name}`, status: 'ACTIVE', branchId, createdById: req.user.id, }, } : undefined, }, }); // 方式二:从 Transaction 创建,同时创建 Account(适合以交易为主场景) await prisma.transaction.create({ data: { type: 'OPENING_BALANCE', amount: Number(balance), reference: uuidv4(), description: `Account opening balance for ${name?.trim()} created by ${req.user.name}`, status: 'ACTIVE', branchId, createdById: req.user.id, account: { create: { accountCode: Number(accountCode), name: name?.trim() ?? '', type, description: description ?? '', balance: Number(balance), status, branchId, createdById: req.user.id, }, }, }, });
✅ 优势:代码更简短、声明式、Prisma 自动处理外键和事务;
⚠️ 注意:需确保 Prisma Schema 中定义了正确的关系(@relation),否则嵌套写入将报错。
❌ 错误写法回顾与纠正
原代码中 accountId: prisma.account.fields.id 是典型误区:
- prisma.account.fields 是静态字段元信息(如 { id: { isId: true, … } }),不是运行时值容器;
- 数组式事务 [promise1, promise2] 中各 Promise 并行执行,无法跨 Promise 传递数据;
- 即使 prisma.account.create(…) 先完成,prisma.transaction.create(…) 也无法访问其返回值。
总结建议
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 需严格控制执行顺序、含条件逻辑或额外业务校验 | ✅ 回调式 $transaction | 类型安全、可读性强、支持任意 js 控制流 |
| 账户与交易强绑定、无复杂中间逻辑 | ✅ 嵌套写入(create + account.transactions.create) | 更少代码、更高性能、自动事务保障 |
| 需批量创建多个交易(如期初余额+手续费) | ✅ createMany 嵌套 | 如 transactions: { createMany: [{}, {}] } |
无论采用哪种方式,请始终配合 Prisma 官方 Schema 关系定义,并在开发中启用 strict: true 和 typescript 类型检查,以提前捕获外键引用错误。