如何封装asyncHandler自动处理Mongoose事务消除重复代码
完全可以通过包装器收敛所有事务相关样板代码,不需要在每个业务函数里重复编写会话创建、事务启停、回滚提交逻辑,且能完全兼容你现有的asyncHandler错误处理体系。
实现方案
核心逻辑是把事务生命周期的固定逻辑全部抽到公共包装函数中,业务代码只需要关心核心操作,按需取用包装器注入的session实例即可。
方案1:独立事务包装器(无侵入,不改动原有asyncHandler)
新建独立的事务包装中间件,和原有asyncHandler组合使用,不需要修改现有已上线的代码逻辑:
import { startSession } from 'mongoose'; const withTransaction = (fn) => async (req, res, next) => { const session = await startSession(); session.startTransaction(); try { // 把session注入到业务函数的入参中 await fn(req, res, next, session); // 业务无异常自动提交 await session.commitTransaction(); } catch (err) { // 异常自动回滚 await session.abortTransaction(); // 错误透传给全局错误处理链路 next(err); } finally { // 无论成功失败都释放会话资源,避免连接泄漏 session.endSession(); } }; export default withTransaction;
使用方式
业务函数外层同时套asyncHandler和withTransaction即可,session会作为第四个参数传入业务函数,直接传给Mongoose的操作方法:
import Company from '../models/Company'; import Person from '../models/Person'; import User from '../models/User'; import Mandate from '../models/Mandate'; import asyncHandler from '../middleware/asyncHandler'; import withTransaction from '../middleware/withTransaction'; export const register = asyncHandler(withTransaction(async (req, res, next, session) => { let entity; if(req.body.profile_type === 'company') { entity = (await Company.create([{ ...req.body }], { session }))[0]; } else { entity = (await Person.create([{ ...req.body }], { session }))[0]; } const user = (await User.create([{ entity, ...req.body }], { session }))[0]; await Mandate.create([{ entity, status: 'unsigned' }], { session }); const verification_token = user.generateVerificationToken(); await sendAccountVerificationMail(user.email, user.first_name, user.language, verification_token); res.json({ message: 'User succesfully registered. Check your mailbox to verify your account and continue the onboarding.', }) }));
注:Mongoose中给
create传数组+session参数时,返回值是数组格式,取第一个元素就是创建的文档实例,和原有逻辑保持一致。
方案2:整合到原有asyncHandler(使用更简洁)
如果不想嵌套两层包装,可以直接把事务逻辑整合到现有asyncHandler中,通过参数控制是否开启事务,对不需要事务的普通接口完全无侵入:
import { startSession } from 'mongoose'; /** * 异步处理包装器 * @param {Function} fn 业务处理函数 * @param {boolean} enableTransaction 是否开启Mongoose事务,默认关闭 */ const asyncHandler = (fn, enableTransaction = false) => async (req, res, next) => { // 非事务接口走原有逻辑 if (!enableTransaction) { return Promise.resolve(fn(req, res, next)).catch(next); } // 事务接口自动处理完整生命周期 const session = await startSession(); session.startTransaction(); try { await fn(req, res, next, session); await session.commitTransaction(); } catch (err) { await session.abortTransaction(); next(err); } finally { session.endSession(); } }; export default asyncHandler;
使用方式
需要事务的接口在调用asyncHandler时传入第二个参数为true即可,普通接口写法和之前完全一致:
// 需要事务的注册接口 export const register = asyncHandler(async (req, res, next, session) => { // 核心业务逻辑和上面示例一致,直接使用入参里的session }, true); // 不需要事务的普通查询接口,原有写法不用改 export const getProfile = asyncHandler(async (req, res, next) => { const user = await User.findById(req.user.id); res.json({ data: user }); });
注意事项
- 包装器只是收敛了事务的样板代码,MongoDB事务本身的限制依然存在:需要MongoDB运行在副本集或分片集群模式下,单节点实例无法执行事务
- 所有需要加入事务的数据库操作,必须显式传入
{ session }参数,否则操作不会加入当前事务,异常时无法回滚 - 不要在事务中执行耗时过长的操作(比如调用第三方接口超时),会导致事务锁占用时间过长影响数据库性能,示例中发邮件的操作如果耗时不稳定,可以考虑调整到事务提交之后执行
内容的提问来源于stack exchange,提问作者Thore
相关产品推荐
相关产品推荐

