Express.js中如何用MongoDB Session对象封装控制器(TypeScript)
优雅统一管理Express+TypeScript项目中的MongoDB事务Session
针对你在每个控制器中重复编写事务逻辑的问题,推荐以下几种架构层面的优化方案,避免代码冗余同时保证逻辑的一致性:
方案一:Express中间件统一托管事务
利用Express中间件的特性,将事务的创建、提交/回滚、会话关闭逻辑集中处理,把MongoDB Session挂载到请求对象上,供控制器直接使用。
1. 编写事务中间件
import { Request, Response, NextFunction } from 'express'; import mongoose from 'mongoose'; // 扩展Express Request类型,避免类型断言 declare global { namespace Express { interface Request { mongoSession: mongoose.ClientSession; } } } // 支持传入事务配置选项的中间件工厂函数 export const transactionMiddleware = (options?: mongoose.TransactionOptions) => { return async (req: Request, res: Response, next: NextFunction) => { const session = await mongoose.startSession(); session.startTransaction(options); req.mongoSession = session; try { // 等待控制器逻辑执行完成 await next(); // 无异常则提交事务 await session.commitTransaction(); } catch (err) { // 捕获异常回滚事务 await session.abortTransaction(); // 将异常传递给全局错误处理中间件 next(err); } finally { // 无论成功失败,关闭会话 session.endSession(); } }; };
2. 控制器中使用Session
export const requestHandler = async (req: Request, res: Response) => { const { params } = req.body; // 直接从请求对象获取Session const session = req.mongoSession; const someData = await someService.getData(params, session); const response = await someOtherService.updateData(someData, session); return res.status(200).json(response); };
3. 路由配置
可以全局挂载中间件,或针对需要事务的路由/路由组单独配置:
import express from 'express'; const app = express(); // 全局挂载,所有/api前缀的请求都启用事务 app.use('/api', transactionMiddleware()); // 单个路由自定义事务选项 app.post('/api/critical-update', transactionMiddleware({ writeConcern: { w: 'majority' }, readConcern: { level: 'snapshot' } }), requestHandler);
方案二:高阶函数包裹控制器
如果不想全局或批量挂载中间件,可编写高阶函数,为需要事务的控制器单独添加事务逻辑,保持控制器代码的简洁性。
1. 编写高阶函数
import { Request, Response, NextFunction } from 'express'; import mongoose from 'mongoose'; type Controller = (req: Request, res: Response, next: NextFunction) => Promise<any>; export const withTransaction = (controller: Controller, options?: mongoose.TransactionOptions) => { return async (req: Request, res: Response, next: NextFunction) => { const session = await mongoose.startSession(); session.startTransaction(options); (req as any).mongoSession = session; try { await controller(req, res, next); await session.commitTransaction(); } catch (err) { await session.abortTransaction(); next(err); } finally { session.endSession(); } }; };
2. 包裹控制器
// 用高阶函数包裹原控制器逻辑 export const requestHandler = withTransaction(async (req, res) => { const { params } = req.body; const session = (req as any).mongoSession; const someData = await someService.getData(params, session); const response = await someOtherService.updateData(someData, session); return res.status(200).json(response); }, { writeConcern: { w: 'majority' } }); // 可选传入事务配置
3. 路由配置
直接使用包裹后的控制器即可:
app.post('/api/update', requestHandler);
方案三:TypeScript装饰器(类式控制器场景)
如果你的项目采用类式组织控制器(如NestJS风格),可以用TS装饰器为方法注入事务逻辑,实现声明式的事务管理。
1. 编写事务装饰器
import mongoose from 'mongoose'; import { Request } from 'express'; function Transaction(options?: mongoose.TransactionOptions) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod = descriptor.value; descriptor.value = async function (...args: any[]) { const session = await mongoose.startSession(); session.startTransaction(options); try { // 从控制器方法参数中获取Request对象 const req = args.find(arg => arg instanceof Request); if (req) req.mongoSession = session; const result = await originalMethod.apply(this, args); await session.commitTransaction(); return result; } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); } }; return descriptor; }; }
2. 在类控制器中使用
import { Request, Response } from 'express'; class UserController { @Transaction({ writeConcern: { w: 'majority' } }) async updateUser(req: Request, res: Response) { const { userId, data } = req.body; const session = req.mongoSession; const user = await userService.getUser(userId, session); const updatedUser = await userService.updateUser(user, data, session); return res.status(200).json(updatedUser); } }
关键注意事项
- 类型安全:通过扩展Express Request接口,避免使用
any类型断言,保证TS类型检查的有效性。 - 错误传递:确保事务回滚后,异常能正确传递到Express的全局错误处理中间件,避免静默失败。
- 灵活配置:所有方案都支持传入MongoDB事务选项,满足不同业务场景的需求(如强一致性要求的写关注级别)。
- 非事务请求:对于不需要事务的控制器,直接跳过中间件/高阶函数/装饰器即可,无需修改核心逻辑。
内容的提问来源于stack exchange,提问作者Shaun
相关产品推荐
相关产品推荐

