
本文详解在 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 类型检查,以提前捕获外键引用错误。










