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

MongoDB与Mongoose:如何关联Expense到Budget文档数组内的分类?

问题描述

我使用MongoDB刚满一个月,也缺乏关系型数据库经验,现在有如下需求和疑问:

我定义了Mongoose的Budget Schema:

import mongoose, { model, Schema } from "mongoose";
import { IBudget } from "../types/budget";
import User from "./user.model";

const budgetSchema = new Schema<IBudget>({
  user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
  categories: {
    type: [
      {
        title: { type: "string", required: true, trim: true },
        amount: { type: "number", required: true, default: 0 },
        color: { type: "string", required: true },
        managed: { type: "boolean", required: true, default: true },
        editable: { type: "boolean", required: true, default: true },
        description: { type: "string", maxlength: 120 },
      },
    ],
    required: true,
    maxlength: [12, "Cannot have more than 12 categories"],
    minlength: [1, "Need at least 1 category"],
  },
  month: {
    type: Number,
    required: [true, "Please add month"],
    min: 1,
    max: 12,
  },
  year: { type: Number, required: [true, "Please add the year"] },
});

const Budget = model("Budget", budgetSchema);

export default Budget;

通过Budget.create({...budgetObject})创建文档后,数据库中会生成对应结构的Budget文档。

另外还有Expense Schema:

import mongoose, { model, Schema } from "mongoose";
import { IExpense } from "../types/expense";
import User from "./user.model";

const expenseSchema = new Schema<IExpense>(
  {
    title: { type: String, required: true, trim: true, maxlength: 40 },
    description: { type: String, trim: true, maxlength: 260 },
    expenseDate: { type: Date, default: Date.now },
    category: {
      type: String,
      required: [true, "Please add the category name"],
    },
    user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
    amount: { type: Number, required: true },
    reverted: { type: Boolean, required: true, default: false },
  },
  { timestamps: false }
);

const Expense = model("Expense", expenseSchema);
export default Expense;

我的疑问是:如何将Expense文档中的category字段关联到Budget文档categories数组中的某一项?

我知道如何用{ type: mongoose.Schema.Types.ObjectId, required: true, ref: User }关联其他独立文档,但不清楚怎么关联嵌套在另一文档数组里的子文档,不确定这种关联是否可行,如果不可行请提供替代方案。


解决方案

方案一:直接关联嵌套子文档(可行,但有局限)

MongoDB的嵌套子文档默认会自动生成_id(除非显式禁用),所以你可以通过子文档的_id关联,再结合用户和预算年月定位对应的Budget文档。

  1. 确认子文档的_id存在
    你的Schema没有禁用子文档的_id,所以每个categories数组项都会自动生成唯一_id,结构示例:

    {
      "_id": ObjectId("预算文档ID"),
      "user": ObjectId("用户ID"),
      "categories": [
        {
          "_id": ObjectId("子文档ID"), // 自动生成
          "title": "餐饮",
          "amount": 2000,
          // ... 其他字段
        }
      ],
      "month": 10,
      "year": 2024
    }
    
  2. 修改Expense Schema
    将category字段改为存储子文档_id,同时添加预算年月字段用于定位Budget:

    category: {
      type: mongoose.Schema.Types.ObjectId,
      required: [true, "Please add the category ID"],
      // 注意:ref无法直接指向嵌套数组,需手动关联
    },
    budgetMonth: { type: Number, required: true, min:1, max:12 },
    budgetYear: { type: Number, required: true }
    
  3. 查询时手动关联分类信息
    先查询Expense,再根据用户ID、预算年月找到对应Budget,最后从categories数组匹配子文档:

    const getExpenseWithCategory = async (expenseId) => {
      const expense = await Expense.findById(expenseId).populate('user');
      if (!expense) return null;
    
      const budget = await Budget.findOne({
        user: expense.user._id,
        month: expense.budgetMonth,
        year: expense.budgetYear
      });
    
      const category = budget.categories.find(cat => cat._id.toString() === expense.category.toString());
      return { ...expense.toObject(), category };
    };
    

    局限:

    • 无法使用Mongoose自动populate,必须手动查询匹配
    • 若Budget中的分类被删除,对应Expense会出现无效_id,需额外处理数据一致性

方案二:将分类抽为独立集合(更推荐,适合长期扩展)

如果分类需要复用或灵活查询,建议把Category做成独立模型,像关联User一样实现轻松关联。

  1. 创建独立Category Schema

    import mongoose, { model, Schema } from "mongoose";
    import { ICategory } from "../types/category";
    import User from "./user.model";
    
    const categorySchema = new Schema<ICategory>({
      user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
      title: { type: "string", required: true, trim: true },
      amount: { type: "number", required: true, default: 0 },
      color: { type: "string", required: true },
      managed: { type: "boolean", required: true, default: true },
      editable: { type: "boolean", required: true, default: true },
      description: { type: "string", maxlength: 120 },
    });
    
    const Category = model("Category", categorySchema);
    export default Category;
    
  2. 修改Budget Schema
    让categories存储Category的_id数组:

    const budgetSchema = new Schema<IBudget>({
      user: { type: mongoose.Schema.Types.ObjectId, required: true, ref: User },
      categories: {
        type: [mongoose.Schema.Types.ObjectId],
        ref: "Category",
        required: true,
        maxlength: [12, "Cannot have more than 12 categories"],
        minlength: [1, "Need at least 1 category"],
      },
      month: {
        type: Number,
        required: [true, "Please add month"],
        min: 1,
        max: 12,
      },
      year: { type: Number, required: [true, "Please add the year"] },
    });
    
  3. 修改Expense Schema
    直接关联Category模型:

    category: {
      type: mongoose.Schema.Types.ObjectId,
      required: [true, "Please add the category ID"],
      ref: "Category"
    },
    
  4. 查询时自动populate分类
    用Mongoose自带的populate即可快速获取分类详情:

    const expense = await Expense.findById(expenseId)
      .populate('user')
      .populate('category');
    

    优势:

    • 代码简洁,支持自动关联查询
    • 数据一致性易维护(比如删除分类时可通过中间件处理关联Expense)
    • 分类可被多个Budget复用,更符合MongoDB灵活设计

方案三:用分类名称+用户+预算年月关联(最简单,适合新手)

如果暂时不想大幅改动结构,可保留Expense的category为分类名称,同时添加预算年月字段,查询时通过用户ID、年月和分类名称匹配Budget中的分类:

const getExpenseWithCategory = async (expenseId) => {
  const expense = await Expense.findById(expenseId).populate('user');
  if (!expense) return null;

  const budget = await Budget.findOne({
    user: expense.user._id,
    month: expense.budgetMonth,
    year: expense.budgetYear
  });

  const category = budget.categories.find(cat => cat.title === expense.category);
  return { ...expense.toObject(), category };
};

缺点:若分类名称被修改,所有关联Expense会出现匹配问题,仅适合分类名称固定的场景。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 00:35:26