You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 19:30:35