Mongoose批量更新符合条件文档的最佳实践(结合node-cron)
批量更新Mongoose文档的最佳实践
问题场景
通过node-cron定时执行函数,从数据库获取所有Appointment文档,将满足「当前时间超过预约结束时间」条件的文档的active字段设为false,再批量更新回数据库。现有代码中,Appointment.find()返回Mongoose文档数组,无法直接调用.save()方法。
现有实现代码:
const Appointment = require("../models/Appointment"); const { getDate, getMinutes, getHours, getYear, getMonth, addMonths, } = require("date-fns"); const retrieveAllAppointmentsCron = async () => { const date = new Date(); const year = getYear(date); const month = getMonth(date) + 1; const day = getDate(date); const hour = getHours(date); const minutes = getMinutes(date); if (month < 10) month = "0" + month; if (day < 10) day = "0" + day; if (hour < 10) hour = "0" + hour; if (minutes < 10) minutes = "0" + minutes; const nowFormDate = new Date(`${year}-${month}-${day}T${hour}:${minutes}`); const res = await Appointment.find() .select("-shopId -customerName -service -comments -email ") .exec(); let cancleCounter=0; res.map((app) => { var appFormEndDate = new Date(`${app.date}T${app.endTime}`); if(nowFormDate>appFormEndDate) { app.active = false; cancleCounter++; } }); await res.save() console.log(`a total of ${cancleCounter} appointments where updated to completed`) cancleCounter=0; }; module.exports = { retrieveAllAppointmentsCron, };
最佳实践方案
方案1:使用updateMany直接在数据库层面完成(推荐)
这是性能最优的方案,无需将所有文档加载到内存,直接通过MongoDB的查询条件筛选目标文档并批量更新,减少网络IO和内存占用。
实现代码:
const retrieveAllAppointmentsCron = async () => { const nowFormDate = new Date(); // 按业务逻辑生成当前时间 // 批量更新符合条件的文档 const updateResult = await Appointment.updateMany( { // 数据库层面拼接日期与时间,判断是否早于当前时间 $expr: { $lt: [ { $dateFromString: { dateString: { $concat: ["$date", "T", "$endTime"] } } }, nowFormDate ] }, active: true // 只更新未标记为非活跃的文档,避免重复操作 }, { $set: { active: false } } // 执行字段更新 ); console.log(`a total of ${updateResult.modifiedCount} appointments were updated to completed`); };
逻辑解释:
updateMany接收两个核心参数:查询条件和更新操作- 通过
$expr利用MongoDB聚合表达式,直接在数据库层完成日期拼接与比较,无需把数据拉到本地处理 - 增加
active: true的过滤条件,避免重复更新已处理过的文档 - 返回的
modifiedCount直接对应实际更新的文档数量,无需手动计数
方案2:遍历文档数组逐个调用save(适用于复杂内存处理场景)
如果必须将文档加载到内存做额外业务逻辑处理,可以遍历每个文档实例,调用其自身的.save()方法,用Promise.all并行处理提升效率。
实现代码:
const retrieveAllAppointmentsCron = async () => { const nowFormDate = new Date(); // 按业务逻辑生成当前时间 const res = await Appointment.find() .select("-shopId -customerName -service -comments -email ") .exec(); const updatePromises = []; let cancelCounter = 0; res.forEach((app) => { const appFormEndDate = new Date(`${app.date}T${app.endTime}`); if (nowFormDate > appFormEndDate && app.active) { app.active = false; updatePromises.push(app.save()); cancelCounter++; } }); await Promise.all(updatePromises); // 并行执行所有保存操作 console.log(`a total of ${cancelCounter} appointments were updated to completed`); };
逻辑解释:
- Mongoose的单个文档实例自带
.save()方法,但数组本身没有该方法,因此需要遍历每个元素单独调用 - 收集所有待保存的Promise,用
Promise.all并行执行,相比逐个await能大幅节省执行时间 - 增加
app.active判断,避免重复保存已修改过的文档
方案3:使用lean()获取普通JS对象,再用bulkWrite批量更新
如果不需要Mongoose文档的实例方法,可通过lean()将查询结果转为普通JavaScript对象,修改后用bulkWrite批量提交更新操作。
实现代码:
const retrieveAllAppointmentsCron = async () => { const nowFormDate = new Date(); // 按业务逻辑生成当前时间 const res = await Appointment.find() .select("-shopId -customerName -service -comments -email ") .lean() .exec(); // 生成批量更新操作数组 const updateOperations = res .filter(app => { const appFormEndDate = new Date(`${app.date}T${app.endTime}`); return nowFormDate > appFormEndDate && app.active; }) .map(app => ({ updateOne: { filter: { _id: app._id }, update: { $set: { active: false } } } })); const bulkResult = await Appointment.bulkWrite(updateOperations); console.log(`a total of ${bulkResult.modifiedCount} appointments were updated to completed`); };
逻辑解释:
lean()返回普通JS对象,内存占用更小、处理速度更快- 先筛选出需要更新的文档,再生成
bulkWrite要求的操作格式数组 bulkWrite可一次性执行多个更新操作,减少数据库请求次数
内容的提问来源于stack exchange,提问作者Kwstas Greco
相关产品推荐
相关产品推荐

