设计MongoDB Schema的最优方式?工位预订系统Mongoose Schema求优化建议
现有Schema的核心问题
- 缺失核心关联字段:
User表未关联所属公司,Booking表未关联工位、办公室、公司,完全无法满足业务权限控制和多维度查询需求 - 工位预订字段设计错误:
Office.desk.bookings为单值ObjectId,一个工位无法存储多时间段的预订记录 - 双向引用冗余且易出错:同时在
User.bookings、Office.desk.bookings存储预订引用,新增/删除预订时需要同时更新3张表,极易出现数据不一致 - 基础约束缺失:大量业务唯一字段未加唯一索引、必填校验,也未做敏感字段的访问限制
- 缺乏常用扩展字段:没有预订状态、用户角色这类高频使用的属性,后续业务迭代会非常痛苦
优化后的Schema设计方案
核心思路是以Booking为主表存储全量关联信息,取消冗余双向引用,增加必要约束和索引,减少数据维护成本,提升查询效率
const mongoose = require('mongoose') const Schema = mongoose.Schema // 公司表 const CompanySchema = new Schema({ companyName: { type: String, required: true }, companyLogo: String, uniqueCompanyCode: { type: String, required: true, unique: true // 公司编码全局唯一 }, companyPasscode: { type: String, required: true, select: false // 禁止普通查询返回通行码,避免泄露 }, companyAdmins: [{ type: Schema.Types.ObjectId, ref: "User" }], offices: [{ type: Schema.Types.ObjectId, ref: 'Office' }] }, { timestamps: true }) // 自动生成创建/更新时间,后续排查问题更方便 // 用户表 const UserSchema = new Schema({ email: { type: String, required: true, unique: true }, companyId: { type: Schema.Types.ObjectId, ref: 'Company', required: true // 所有用户必须关联所属公司 }, role: { type: String, enum: ['admin', 'employee'], default: 'employee', required: true } }, { timestamps: true }) // passportLocalMongoose会自动生成username、hash、salt字段,无需手动添加 UserSchema.plugin(passportLocalMongoose); // 办公室表 const OfficeSchema = new Schema({ companyId: { type: Schema.Types.ObjectId, ref: 'Company', required: true }, officeAddress:{ streetAddress: String, town: String, county: String, postcode: String }, floorPlan: String, desks: [{ deskNumber: { type: Number, required: true }, description: String // 可选,可存储靠窗、带显示器等工位描述 }] }, { timestamps: true }) // 同办公室工位号唯一的复合索引,避免重复工位号 OfficeSchema.index({ companyId: 1, 'desks.deskNumber': 1 }, { unique: true }) // 预订表(核心主表) const BookingSchema = new Schema({ companyId: { type: Schema.Types.ObjectId, ref: 'Company', required: true }, officeId: { type: Schema.Types.ObjectId, ref: 'Office', required: true }, deskId: { type: Schema.Types.ObjectId, required: true // 对应Office.desks数组里的_id }, deskNumber: { type: Number, required: true // 冗余存储工位号,查询预订时无需关联Office表直接展示 }, bookedFrom: { type: Date, required: true }, bookedTo: { type: Date, required: true }, bookedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, status: { type: String, enum: ['active', 'cancelled', 'completed'], default: 'active', required: true } }, { timestamps: true }) // 高频查询索引,大幅提升查询速度 BookingSchema.index({ deskId: 1, bookedFrom: 1, bookedTo: 1 }) // 预订冲突校验专用索引 BookingSchema.index({ bookedBy: 1, bookedFrom: -1 }) // 用户查个人预订按时间倒序 BookingSchema.index({ officeId: 1, bookedFrom: -1 }) // 管理员查办公室预订按时间倒序 // 预订时间校验中间件,保存前自动判断结束时间晚于开始时间 BookingSchema.pre('save', function(next) { if (this.bookedTo <= this.bookedFrom) { next(new Error('预订结束时间必须晚于开始时间')) } next() })
对应查询实现(完全满足多维度查询需求)
- 员工查自己的所有有效预订:
Booking.find({ bookedBy: userId, status: 'active' }).sort({ bookedFrom: -1 }) - 管理员查指定办公场地的所有有效预订:
Booking.find({ officeId: officeId, status: 'active' }).sort({ bookedFrom: -1 }) - 预订前校验时间段冲突:
Booking.find({ deskId: deskId, status: 'active', $or: [{ bookedFrom: { $lt: newBookedTo } }, { bookedTo: { $gt: newBookedFrom } }] }),返回结果不为空则代表时间段冲突 - 无需维护多表引用关联,新增/取消预订仅需操作Booking单表即可,完全避免数据不一致问题
内容的提问来源于stack exchange,提问作者harrymarah
相关产品推荐
相关产品推荐

