Mongoose双集合更新:创建应用时同步更新项目的最优方案
解决方案
嘿,这个需求太典型了——既要保证数据一致性,又不想在业务代码里写重复的多请求逻辑。下面给你三个实用的方案,你可以根据自己的场景选:
方案1:Mongoose 文档中间件(Post Save Hook)
这是最贴近你“业务代码只调用一次”需求的方案,直接在Application模型上添加post-save钩子,当新的Application文档被保存后,自动触发Project的更新逻辑。这样你在业务代码里只需要调用Application.create(),剩下的同步工作由Mongoose帮你完成。
示例代码(修改Application模型):
const mongoose = require('mongoose'); const Project = require('./product'); // 引入Project模型 const applicationSchema = new mongoose.Schema({ applicantId: { type: ObjectId, ref: 'User' }, ownerId: { type: ObjectId, ref: 'User' }, projectId: { type: ObjectId, ref: 'Project' } }, {timestamps: true}); // 添加Post Save钩子 applicationSchema.post('save', async function(doc) { try { // 更新对应Project的申请数和申请人列表 await Project.findByIdAndUpdate( doc.projectId, { $inc: { applications: 1 }, // 申请数+1 $push: { applicants: doc.applicantId } // 把申请人ID加入数组 }, { new: true } // 返回更新后的文档(可选) ); } catch (err) { // 这里要处理错误,比如日志记录或者回滚(注意:post钩子无法回滚已保存的Application) console.error('同步更新Project失败:', err); // 如果你需要强一致性,可能需要结合事务,或者在这里做补偿逻辑 } }); module.exports = mongoose.model("Application", applicationSchema);
优点:业务代码极简,只需要一次create调用;逻辑和模型绑定,维护方便。
注意点:如果save操作成功但Project更新失败,会出现数据不一致(Application已创建但Project没更新),所以一定要做好错误日志和补偿机制;另外,这个钩子只触发单个文档的save,如果用insertMany批量创建,需要单独加post('insertMany')钩子。
方案2:MongoDB 事务(原子操作)
如果你的场景要求强原子性(要么Application创建成功且Project更新成功,要么都失败),那MongoDB事务是最佳选择。事务能保证多个操作在一个会话里要么全部提交,要么全部回滚,彻底避免数据不一致。
注意:MongoDB事务需要部署为副本集或分片集群,单节点MongoDB不支持事务。
示例代码(业务逻辑层):
const mongoose = require('mongoose'); const Application = require('./models/application'); const Project = require('./models/product'); async function createApplicationAndUpdateProject(applicationData) { const session = await mongoose.startSession(); session.startTransaction(); try { // 在事务中创建Application const newApplication = await Application.create([applicationData], { session }); // 在事务中更新对应Project await Project.findByIdAndUpdate( applicationData.projectId, { $inc: { applications: 1 }, $push: { applicants: applicationData.applicantId } }, { session } ); // 提交事务 await session.commitTransaction(); session.endSession(); return newApplication[0]; } catch (err) { // 回滚事务 await session.abortTransaction(); session.endSession(); console.error('创建申请并更新项目失败:', err); throw err; // 抛给上层处理 } } // 调用示例 createApplicationAndUpdateProject({ applicantId: 'xxx', ownerId: 'yyy', projectId: 'zzz' });
优点:绝对的原子性,不会出现数据不一致;业务逻辑集中,便于调试。
注意点:依赖MongoDB的集群部署;事务会有一定性能开销,但对于单条申请的场景完全可以忽略。
方案3:MongoDB 变更流(Change Streams)
这个方案相当于MongoDB端的“触发器”,不需要在业务代码或模型里写任何同步逻辑,直接监听Applications集合的插入事件,一旦有新文档插入,自动触发Project的更新。适合跨服务场景(比如Application由服务A创建,Project由服务B维护),或者不想把同步逻辑和业务代码耦合的情况。
示例代码(可以单独写一个监听服务,或者在项目启动时初始化):
const mongoose = require('mongoose'); const Application = require('./models/application'); const Project = require('./models/product'); async function setupApplicationChangeStream() { // 监听Applications集合的插入事件 const changeStream = Application.watch([ { $match: { operationType: 'insert' } } // 只监听插入操作 ]); changeStream.on('change', async (change) => { const newApplication = change.fullDocument; // 获取新插入的申请文档 try { await Project.findByIdAndUpdate( newApplication.projectId, { $inc: { applications: 1 }, $push: { applicants: newApplication.applicantId } } ); console.log('通过变更流同步Project成功'); } catch (err) { console.error('变更流同步Project失败:', err); // 这里可以加入重试机制,比如把失败的任务存入消息队列,后续重试 } }); // 处理变更流的错误 changeStream.on('error', (err) => { console.error('变更流监听失败:', err); // 可以在这里重启监听,保证服务可用性 }); } // 启动监听(项目初始化时调用) setupApplicationChangeStream();
优点:完全解耦业务逻辑和同步逻辑;跨服务场景友好;不需要修改业务代码。
注意点:同样依赖MongoDB副本集/分片集群;需要处理监听中断、更新失败的重试逻辑;变更流是异步的,无法保证实时性(但延迟极低)。
内容的提问来源于stack exchange,提问作者Davide Lorino

