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文档。
确认子文档的
_id存在
你的Schema没有禁用子文档的_id,所以每个categories数组项都会自动生成唯一_id,结构示例:{ "_id": ObjectId("预算文档ID"), "user": ObjectId("用户ID"), "categories": [ { "_id": ObjectId("子文档ID"), // 自动生成 "title": "餐饮", "amount": 2000, // ... 其他字段 } ], "month": 10, "year": 2024 }修改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 }查询时手动关联分类信息
先查询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,需额外处理数据一致性
- 无法使用Mongoose自动
方案二:将分类抽为独立集合(更推荐,适合长期扩展)
如果分类需要复用或灵活查询,建议把Category做成独立模型,像关联User一样实现轻松关联。
创建独立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;修改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"] }, });修改Expense Schema
直接关联Category模型:category: { type: mongoose.Schema.Types.ObjectId, required: [true, "Please add the category ID"], ref: "Category" },查询时自动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

