Node.js中Mongoose的remove()不是函数,级联删除训练营课程失败
bootcamp.remove()报错及级联删除实现方案 我用Node.js和Mongoose开发训练营(bootcamp)与课程(course)的API,二者为一对多关系,希望实现删除训练营时自动删除其关联的所有课程。
在Bootcamp模型中定义了pre('remove')中间件用于级联删除,同时配置了虚拟属性反向关联课程:
// Cascade delete courses when a bootcamp is deleted BootcampSchema.pre('remove', async function(next) { console.log(`Courses being removed from bootcamp ${this._id}`); await this.model('Course').deleteMany({ bootcamp: this._id }); next(); }); // Reverse populate with virtuals BootcampSchema.virtual('courses', { ref: 'Course', localField: '_id', foreignField: 'bootcamp', justOne: false });
控制器bootcamps.js中的删除逻辑如下:
// @desc Delete a Bootcamp // @route DELETE /api/v1/bootcamps/:id // @access Private exports.deleteBootcamp = asyncHandler(async (req, res, next) => { const bootcamp = await Bootcamp.findById(req.params.id); if(!bootcamp) { return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404)); } bootcamp.remove(); res.status(200).json({ success: true, data: {} }); })
但通过Postman发送请求时返回错误:bootcamp.remove is not a function,想了解错误原因及其他级联删除方案。
错误原因
Mongoose从6.x版本开始,移除了文档实例的remove()方法,替换为deleteOne()或deleteMany()方法。你当前使用的Mongoose版本应该是6.x及以上,所以调用bootcamp.remove()会触发该错误。
修复当前方案
把控制器中的bootcamp.remove()替换为await bootcamp.deleteOne(),同时必须用await等待删除操作完成,否则中间件可能无法正常触发:
exports.deleteBootcamp = asyncHandler(async (req, res, next) => { const bootcamp = await Bootcamp.findById(req.params.id); if(!bootcamp) { return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404)); } // 替换为deleteOne()并添加await await bootcamp.deleteOne(); res.status(200).json({ success: true, data: {} }); })
修改后,pre('remove')中间件会正常触发,自动删除关联的课程。
其他级联删除方案
1. 结合findByIdAndDelete与查询级中间件
如果不想先查询文档再删除,可以直接使用findByIdAndDelete方法,但需要将中间件从pre('remove')改为查询级的pre('deleteOne'),因为findByIdAndDelete会触发查询级的deleteOne钩子:
// 修改Bootcamp模型的中间件 BootcampSchema.pre('deleteOne', { document: false, query: true }, async function(next) { // 获取查询条件中的训练营ID const bootcampId = this.getQuery()._id; console.log(`Courses being removed from bootcamp ${bootcampId}`); await this.model('Course').deleteMany({ bootcamp: bootcampId }); next(); }); // 控制器简化为: exports.deleteBootcamp = asyncHandler(async (req, res, next) => { const bootcamp = await Bootcamp.findByIdAndDelete(req.params.id); if(!bootcamp) { return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404)); } res.status(200).json({ success: true, data: {} }); })
2. 手动实现级联删除
在控制器中先删除关联的课程,再删除训练营,不需要依赖Mongoose中间件:
exports.deleteBootcamp = asyncHandler(async (req, res, next) => { const bootcamp = await Bootcamp.findById(req.params.id); if(!bootcamp) { return next(new ErrorResponse(`Bootcamp not found with id of ${req.params.id}`, 404)); } // 先删除该训练营下的所有课程 await Course.deleteMany({ bootcamp: req.params.id }); // 再删除训练营本身 await Bootcamp.findByIdAndDelete(req.params.id); res.status(200).json({ success: true, data: {} }); })
这种方式逻辑直观,但如果需要强一致性,建议用Mongoose事务包裹两个删除操作,避免出现删了课程但没删训练营的情况。
3. MongoDB原生级联删除(推荐生产环境)
如果你的MongoDB版本在4.4及以上,可以直接在Course模型的外键字段上配置onDelete: 'CASCADE',由数据库底层自动处理级联删除,这是最可靠的方案:
// Course模型定义 const CourseSchema = new mongoose.Schema({ // 其他字段... bootcamp: { type: mongoose.Schema.Types.ObjectId, ref: 'Bootcamp', required: true, index: true, // 配置级联删除 onDelete: 'CASCADE' } });
配置后,当Bootcamp文档被删除时,MongoDB会自动删除所有关联的Course文档,无需在应用层编写任何额外逻辑。
内容的提问来源于stack exchange,提问作者tukitooo1998

