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

如何使用Mongoose Virtuals实现考勤数据分表存储与关联查询

1. 定义Mongoose Schema

首先定义两个集合对应的Schema,配置虚拟字段关联逻辑:

const mongoose = require('mongoose');
const { Schema } = mongoose;

// timesheet-data集合Schema
const TimesheetDataSchema = new Schema({
  user: {
    type: Schema.Types.ObjectId,
    ref: 'User', // 替换为你自己的用户Model名
    required: true
  },
  timesheetMainId: {
    type: Schema.Types.ObjectId,
    ref: 'TimesheetMain',
    required: true
  },
  date: {
    type: String,
    required: true
  },
  present: {
    type: Boolean,
    required: true
  }
});
const TimesheetData = mongoose.model('TimesheetData', TimesheetDataSchema);

// timesheet-main集合Schema
const TimesheetMainSchema = new Schema({
  user: {
    type: Schema.Types.ObjectId,
    ref: 'User',
    required: true
  },
  week: {
    type: Number,
    required: true
  },
  year: {
    type: Number,
    required: true
  }
}, {
  // 必须开启该配置,虚拟字段才会在返回结果中显示
  toJSON: { virtuals: true },
  toObject: { virtuals: true }
});

// 定义关联子表的虚拟字段data
TimesheetMainSchema.virtual('data', {
  ref: 'TimesheetData', // 关联的子表Model名
  localField: '_id', // 主表用于关联的字段
  foreignField: 'timesheetMainId', // 子表用于关联的对应字段
  justOne: false // 一对多关联,返回数组所以设为false
});

// 封装静态方法:一次性写入主表+子表数据,保证事务一致性
TimesheetMainSchema.statics.createWithData = async function (mainInfo, dataList) {
  const session = await mongoose.startSession();
  session.startTransaction();
  try {
    // 写入主表数据
    const [createdMain] = await this.create([mainInfo], { session });
    // 格式化子表数据,绑定关联字段
    const formattedData = dataList.map(item => ({
      ...item,
      user: createdMain.user,
      timesheetMainId: createdMain._id
    }));
    // 批量写入子表数据
    await TimesheetData.insertMany(formattedData, { session });
    await session.commitTransaction();
    session.endSession();
    return createdMain;
  } catch (err) {
    await session.abortTransaction();
    session.endSession();
    throw err;
  }
};
const TimesheetMain = mongoose.model('TimesheetMain', TimesheetMainSchema);

2. 数据写入示例

直接调用封装好的静态方法即可完成拆分写入,无需手动分两次插入:

// 你的原始业务数据(注意修正原始JSON中2021后面缺失的逗号)
const rawData = {
  "user": "实际用户的ObjectId字符串",
  "week": 21,
  "year": 2021,
  "data": [
    { "date": "01/01/21", "present": true },
    { "date": "02/01/21", "present": false }
  ]
};

// 调用写入方法
const newTimesheet = await TimesheetMain.createWithData(
  { user: rawData.user, week: rawData.week, year: rawData.year },
  rawData.data
);

3. 整合查询示例

查询时通过populate自动关联子表数据,直接返回你要求的整合结构:

// 按用户+周维度查询
const result = await TimesheetMain.findOne({
  user: "目标用户ObjectId",
  week: 21,
  year: 2021
}).populate('data');

// 返回的result结构与你要求的完全一致:
// {
//   "_id": "asf5sf44sdw3sfsf",
//   "user": "abc对应的用户ObjectId",
//   "week": 21,
//   "year": 2021,
//   "data": [
//     { "_id": "Asdfsdfsdfsdfsdfsf", "date": "01/01/21", "present": true },
//     { "_id": "egdfgfhfghfghfgh", "date": "02/01/21", "present": false }
//   ]
// }

注意事项

  • 如果不需要返回__v版本字段,可以在Schema配置中添加versionKey: false
  • 若需要过滤子表返回的字段,可以在populate时指定:populate('data', 'date present -_id'),示例中是只返回date、present字段,排除_id

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.19 09:15:03