mongoose pre('save')钩子内调用save是否触发循环及排序问题咨询
问题解答
核心问题:mongooseSchema.pre('save')中调用model.save()是否会触发循环?
会触发循环,但并非无限循环——mongoose对同一个文档实例的重复save()调用会做内部防抖,不会重复触发当前文档的pre('save')钩子;但你在钩子中调用了其他category文档的save(),这些文档会各自触发自己的pre('save')钩子,每个钩子又会重新查询所有category并调整order,导致多次嵌套执行,最终order逻辑混乱,出现你看到的间隔或不合理数值。
相关问题1:如何解决排序异常问题?
你的核心问题在于用pre('save')钩子执行全局order调整,这种设计本身容易触发循环且逻辑不可控。推荐以下两种解决方案:
方案1:移除pre钩子,在业务层处理order逻辑
在创建/更新category的业务代码中,先完成全局order的调整,再执行保存操作,彻底避开钩子的嵌套触发:
// 创建新分类的业务函数 async function createCategory(categoryData) { const session = await mongoose.startSession(); session.startTransaction(); try { let { order } = categoryData; // 处理负数order,默认设为最后一位+1 if (order <= 0) { const maxOrderDoc = await Category.findOne({}, { order: 1 }).sort({ order: -1 }).session(session); order = maxOrderDoc ? maxOrderDoc.order + 1 : 0; } // 给所有 >= 当前order的分类的order+1,腾出位置 await Category.updateMany( { order: { $gte: order } }, { $inc: { order: 1 } }, { session, runValidators: true } ); // 创建并保存新分类 const newCategory = new Category({ ...categoryData, order }); await newCategory.save({ session }); await session.commitTransaction(); return newCategory; } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); } }
方案2:若坚持用钩子,避免触发其他文档的钩子
改用updateOne而非save()修改其他category的order,因为updateOne默认不触发mongoose中间件,不会引发循环:
categorySchema.pre('save', async function() { const session = await mongoose.startSession(); session.startTransaction(); try { const categories = await Category.find({}).sort({ order: 'asc' }).session(session); if (this.order <= 0) { this.order = 0; // 批量更新所有分类的order为连续值 const bulkOps = categories.map((_, index) => ({ updateOne: { filter: { _id: categories[index]._id }, update: { $set: { order: index + 1 } } } })); await Category.bulkWrite(bulkOps, { session }); } else { // 批量更新 >= 当前order的分类,order递增 let i = this.order + 1; const bulkOps = categories .filter(item => item.order >= this.order) .map(item => ({ updateOne: { filter: { _id: item._id }, update: { $set: { order: i++ } } } })); await Category.bulkWrite(bulkOps, { session }); } await session.commitTransaction(); } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); } });
相关问题2:批量保存的方法,bulkWrite和逐个save的区别?
bulkWrite和逐个save()的核心差异:
- 性能:
bulkWrite将多个操作打包成一个MongoDB请求发送,比逐个save()的多次请求效率高很多,数据量越大差距越明显。 - 中间件触发:默认情况下,
bulkWrite不会触发mongoose的pre('save')/post('save')等钩子;而逐个save()会触发每个文档的钩子。如果需要在bulkWrite中执行验证逻辑,可添加{ runValidators: true }选项,但仍不会触发钩子。 - 原子性:结合MongoDB事务(session),
bulkWrite能保证所有操作要么全部成功,要么全部失败;逐个save()若不用事务,可能出现部分成功部分失败的情况。
你的场景中,bulkWrite既能避免触发其他文档的pre('save')钩子、解决循环问题,又能提升性能,是更优选择。
内容的提问来源于stack exchange,提问作者Afik Habaz
相关产品推荐
相关产品推荐

