如何为每个GraphQL请求实现数据库事务
实现Apollo GraphQL请求对应单次PostgreSQL事务(基于Prisma)
核心思路
通过Apollo的**请求上下文(Context)**绑定Prisma交互式事务,让单个GraphQL请求中所有的mutation/query操作都共享同一个数据库事务:
- 请求进入时,启动Prisma交互式事务,将事务实例注入上下文
- 所有Resolver都使用上下文里的事务实例执行数据库操作
- 请求正常完成时提交事务,出现任何错误则回滚事务
完整代码示例
1. 依赖准备
确保已安装必要依赖:
npm install @apollo/server graphql prisma @prisma/client
2. Prisma配置(schema.prisma)
确保你的Prisma schema配置了PostgreSQL:
generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // 示例业务模型 model User { id Int @id @default(autoincrement()) name String email String @unique } model Post { id Int @id @default(autoincrement()) title String author User @relation(fields: [authorId], references: [id]) authorId Int }
3. Apollo服务器配置(带事务上下文)
import { ApolloServer } from '@apollo/server'; import { startStandaloneServer } from '@apollo/server/standalone'; import { PrismaClient, Prisma } from '@prisma/client'; const prisma = new PrismaClient(); // GraphQL Schema定义 const typeDefs = ` type User { id: Int! name: String! email: String! } type Post { id: Int! title: String! author: User! } type Mutation { createUserWithPost(name: String!, email: String!, postTitle: String!): Post! } `; // Resolvers:所有数据库操作必须使用上下文的事务实例 const resolvers = { Mutation: { createUserWithPost: async (_, args, context) => { const user = await context.tx.user.create({ data: { name: args.name, email: args.email, }, }); const post = await context.tx.post.create({ data: { title: args.postTitle, authorId: user.id, }, include: { author: true }, }); return post; }, }, }; // 创建请求上下文:初始化交互式事务 async function createContext() { const tx = await prisma.$transaction(async (tx) => { // 仅启动事务,返回事务实例供resolver使用 return tx; }, { // 可选:配置事务隔离级别(PostgreSQL支持的级别) isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted, }); return { tx }; } // 启动Apollo服务器,配置事务生命周期管理 async function startServer() { const server = new ApolloServer({ typeDefs, resolvers, plugins: [{ async requestDidStart() { return { async willSendResponse({ context, errors }) { if (errors) { // 请求出错时主动回滚事务 await context.tx.$rollback(); } // 无错误时Prisma交互式事务会自动提交,无需额外操作 }, }; }, }], }); const { url } = await startStandaloneServer(server, { context: async () => createContext(), }); console.log(`🚀 Server ready at ${url}`); } startServer().catch((err) => console.error(err));
关键细节
- 事务实例唯一性:所有Resolver必须使用
context.tx执行数据库操作,不能用全局的prisma客户端,否则操作会脱离当前事务。 - 错误处理:Prisma交互式事务默认会在回调抛出错误时自动回滚,结合Apollo的
willSendResponse插件,可覆盖所有请求级错误场景,确保事务可靠回滚。 - TypeScript类型支持:建议给上下文定义类型,避免类型报错:
type Context = { tx: Prisma.TransactionClient; }; // 初始化Apollo时指定类型 const server = new ApolloServer<Context>({ typeDefs, resolvers, plugins }); - 隔离级别调整:根据业务需求选择合适的事务隔离级别,PostgreSQL支持
ReadUncommitted、ReadCommitted(默认)、RepeatableRead、Serializable。
替代方案
- Apollo Plugin全托管事务:把事务的创建、提交/回滚逻辑完全放到插件中,上下文仅存储事务实例,代码结构更简洁:
plugins: [{ async requestDidStart() { let tx: Prisma.TransactionClient; try { tx = await prisma.$transaction(async (tx) => tx); return { async willSendResponse({ errors }) { if (errors) await tx.$rollback(); }, async context() { return { tx }; }, }; } catch (err) { console.error('事务初始化失败', err); throw err; } }, }], - 不推荐的方案:避免用Prisma Middleware绑定请求级事务,因为middleware无法准确识别GraphQL请求的边界,容易导致事务泄漏或误绑定。
内容的提问来源于stack exchange,提问作者Peter Coppens
相关产品推荐
相关产品推荐

