- Published on
ODM(Object Data Modeling)工具 - mongoose
- Authors
- Name
- Shelton Ma
MongoDB 数据模型
ITicketModel
import mongoose, { Schema } from "mongoose"; import mongoosePaginate from "mongoose-paginate-v2"; import aggregatePaginate from "mongoose-aggregate-paginate-v2"; import User from "./user"; import SecurityEvent from "./securityEvent"; // 枚举类型定义 export const TicketStatusEnum = ["pending", "resolved", "ignored"] as const; export type TicketStatus = [typeof TicketStatusEnum](number); // 复用枚举值生成类型 export interface ITicketModel { _id: string; content: string; // 工单内容 status: TicketStatus; // 工单状态 handler?: string; // 工单处理人 creator: string; // 创建人 securityEvent: string; // 关联安全事件 attachments?: object; // 附件 createdAt: Date; updatedAt: Date; } export type ITicketDocument = ITicketModel & mongoose.Document<string>; // 表结构 const TicketSchema = new Schema<ITicketDocument>( { _id: { type: String, trim: true, required: true, }, content: { type: String, required: true, }, status: { type: String, enum: TicketStatusEnum, default: TicketStatusEnum[0], }, handler: { type: String, ref: User, // 引用 User 表 }, creator: { type: String, ref: User, // 引用 User 表 required: true, // 创建人为必填 }, securityEvent: { type: String, ref: SecurityEvent.modelName, // 引用 SecurityEvent 表 required: true, // 关联安全事件为必填 }, attachments: { type: Object, }, }, { timestamps: true } ); TicketSchema.plugin(mongoosePaginate); TicketSchema.plugin(aggregatePaginate); // 用于在安全事件列表显示工单 TicketSchema.index({ securityEvent: 1, updatedAt: -1 }); const Ticket = mongoose.model< ITicketDocument, mongoose.PaginateModel<ITicketDocument> & mongoose.AggregatePaginateModel<ITicketDocument> >("Ticket", TicketSchema); export default Ticket;
2. 最佳实践
建立索引, 包括复合索引 (Compound Index) , 同时对多个字段进行索引,适用于复杂查询条件.
TicketSchema.index({ securityEvent: 1, updatedAt: -1 });
- 选择正确的索引顺序, 先写筛选条件 (如 securityEvent),再写排序字段 (如 updatedAt) 是最佳实践.
- 避免“范围查询”阻止索引优化 如果 securityEvent 的查询条件为范围查询 (lt),则 updatedAt 排序可能无法充分利用索引.
插件功能 — 扩展 Schema 的能力
- 分页插件, 为 Mongoose 模型提供 paginate() 方法,实现基于查询条件的分页.
TicketSchema.plugin(mongoosePaginate);
- 聚合查询(带分页)
TicketSchema.plugin(aggregatePaginate);
- 分页插件, 为 Mongoose 模型提供 paginate() 方法,实现基于查询条件的分页.
通过
as const
将TicketStatusEnum
转化为字面量类型,以便 enum 直接匹配字符串字面量.export const TicketStatusEnum = ["pending", "resolved", "ignored"] as const; export type TicketStatus = [typeof TicketStatusEnum](number); // 复用枚举值生成类型
主键
_id
类型及自定义MongoDB 默认使用 ObjectId,若使用 number 作为 _id,需确保 ID 生成机制不会出现冲突.
creator: { type: String, ref: User, // 引用 User 表 required: true, // 创建人为必填 },
自动更新
createdAt, updatedAt
{ timestamps: true }
执行顺序
.find() / .findOne() / .findById()
—— 先执行查找.where()
—— 额外的过滤条件.sort()
—— 排序.skip() / .limit()
—— 分页.select()
—— 选择返回的字段.populate()
—— 关联查询.lean()
—— 转换为普通对象.exec()
—— 最后执行查询(可选)
查询封装(
.lean(), .exec(), .populate(), .sort()
)简单查询 ➝ 用 .find() 直接获取数据.
复用查询逻辑 ➝ 用 statics 封装,提高代码复用性和可读性.
TicketSchema.static.findByQuery = function (query: Record<string, any>) { return this.find({ ...query, status: { $ne: "delete" } }); }; TicketSchema.static.findByPlatform = function (platform: string) { return this.find({ platform, status: { $ne: "delete" } }); };
数据预处理 ➝ 在 statics 方法中预处理查询结果,例如 sort()、lean() 或 populate()
TicketSchema.static.findByQuery = function (query: Record<string, any>) { return this.find({ ...query, status: { $ne: "delete" } }) .sort({ createdAt: -1 }) // 时间倒序 .skip((page - 1) * pageSize) // 跳过前面的数据 .limit(pageSize) // 限制每页 10 条 .lean() .exec(); };
逻辑删除
npm install mongoose-delete
import mongooseDelete from "mongoose-delete"; TicketSchema.plugin(mongooseDelete, { overrideMethods: "all", deletedAt: true }); const Ticket = mongoose.model("Ticket", TicketSchema); Ticket.delete(function (err, result) { ... }); Ticket.restore(function (err, result) { ... }); Ticket.find(function (err, documents) { // will return only NOT DELETED documents }); Ticket.findDeleted(function (err, documents) { // will return only DELETED documents }); Ticket.findWithDeleted(function (err, documents) { // will return ALL documents });
methods vs statics
- methods 适用于操作单个文档(如 deleteTicket()).
- statics 适用于批量查询或操作多个文档(如 findActiveTickets()).
- 在一个 Schema 中可以同时使用 methods 和 statics 来实现更加清晰的代码组织.
3. MongoDB nanoid vs ObjectId
1. ObjectId
MongoDB 内置的默认 ID 类型,生成时自动带有时间戳和唯一标识符.
结构 (12 字节 = 24 个十六进制字符):
- 前 4 字节 → 时间戳 (精确到秒)
- 接下来的 5 字节 → 随机值 (确保唯一性)
- 最后 3 字节 → 计数器 (防止时间戳重复)
优点:
- 有时间序列特性,便于按创建时间排序.
- 自动生成,无需手动指定.
- 内存占用更少 (12 字节),索引效率更高.
nanoid
nanoid 是一个轻量级的随机 ID 生成库,具有更快的生成速度和更短的 ID 长度,适用于 URL、客户端生成等场景
_id: { type: String, default: () => nanoid(16) },