You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose中pre钩子不存在报错,如何检测子文档删除时的文档变更?

解决Mongoose Pre钩子无法启用及TypeScript类型报错问题

嘿,看你的代码,你在使用Mongoose pre钩子和TypeScript的时候踩了几个典型的坑,我来帮你理清楚怎么解决:

首先说你代码里的核心问题

  1. Pre钩子不能在请求处理里临时加:Mongoose的中间件(包括pre钩子)必须在模型编译完成前就注册到schema上,你现在在delete接口里才注册pre('save'),这时候模型早就生成好了,钩子根本不会生效。
  2. findOneAndUpdate不触发save钩子:这个方法是直接在数据库层面执行更新操作,不会走模型的save流程,所以哪怕你注册了pre('save'),也不会被触发。
  3. TypeScript类型没配对:你的Category接口没继承Mongoose的Document,所以TypeScript不知道isModified()这些内置方法的存在,自然会报属性不存在的错。

接下来是修正后的完整代码

第一步:完善Schema和类型定义(category.schema.ts)

先把类型补全,然后在模型编译前注册好钩子:

import mongoose, { Schema, Model, Document } from 'mongoose';

// 先定义子文档Item的类型
interface Item {
  item_id: number;
  item_name: string;
}

// 让Category接口继承Document,这样就能拿到Mongoose的所有内置方法
interface Category extends Document {
  category_id: number;
  name: string;
  icon_url: string;
  items_quantity: number;
  items: Item[];
}

const categorySchema = new Schema<Category>({
  category_id: { type: Number, required: true, unique: true },
  name: { type: String, required: true, unique: true },
  icon_url: { type: String, required: true },
  items_quantity: { type: Number, required: true },
  items: [
    {
      item_id: { type: Number, required: true, unique: true },
      item_name: { type: String, required: true }
    }
  ]
});

// 重点:在模型编译前注册pre钩子!
categorySchema.pre('save', function(next) {
  // 这里的this就是当前的Category文档,类型已经自动匹配了
  if (this.isModified()) {
    console.log('文档有变更!');
    // 顺便可以在这里更新items_quantity,比如删除item后自动减1
    this.items_quantity = this.items.length;
  } else {
    console.log('文档没变化~');
  }
  next(); // 一定要调用next()让流程继续
});

// 最后编译模型
const Category: Model<Category> = mongoose.model<Category>('Category', categorySchema);
export default Category;

第二步:修正删除接口的逻辑(路由文件)

因为findOneAndUpdate不触发save钩子,所以我们改成“先查文档→修改内容→手动save”的流程,这样就能触发pre钩子了:

import Category from "../schemas/category.schema";
import { Router } from 'express';
const router = Router();

router.delete('/:category/:item', async (req, res) => {
  try {
    // 1. 先找到对应的分类文档
    const categoryId = parseInt(req.params.category);
    const itemId = parseInt(req.params.item);
    
    const category = await Category.findOne({ category_id: categoryId });
    if (!category) {
      return res.status(404).send('找不到目标分类');
    }

    // 2. 移除指定的item
    category.items = category.items.filter(item => item.item_id !== itemId);

    // 3. 调用save,这时候就会触发我们之前注册的pre钩子了
    const updatedCategory = await category.save();

    res.status(200).send({
      message: `Item ${itemId} 删除成功`,
      updated: updatedCategory
    });
  } catch (err) {
    res.status(500).send(`删除失败:${(err as Error).message}`);
  }
});

export default router;

额外补充:如果想直接用findOneAndUpdate怎么办?

如果你不想改查询逻辑,也可以用pre('findOneAndUpdate')钩子,但要注意这个钩子的this是查询对象,不是文档本身,所以没法直接调用isModified(),示例如下:

// 在category.schema.ts里注册这个钩子
categorySchema.pre('findOneAndUpdate', function(next) {
  const update = this.getUpdate() as any;
  console.log('要执行的更新操作:', update);
  // 这里可以做一些逻辑,比如更新items_quantity,但需要自己计算或者额外查询
  next();
});

不过这种方式没法直接拿到文档的完整数据,如果你需要基于文档当前状态做操作(比如更新items_quantity),还是推荐先查询再save的方式更直观。

内容的提问来源于stack exchange,提问作者MWN

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 07:23:53