
本文详细介绍了如何在 Mongoose 中将 `_id` 字段的数据类型从默认的 `ObjectId` 更改为 `Number`,并实现自动递增的序列号。我们将通过创建自定义 SchemaType 来验证数字 `_id`,并进一步结合预保存钩子和单独的计数器集合,实现 `_id` 字段的原子性自动生成,提供完整的代码示例和最佳实践。
Mongoose 模型默认使用 ObjectId 作为文档的 _id 字段,它是一个 12 字节的唯一标识符,通常能满足大多数应用的需求。然而,在某些特定场景下,开发者可能希望将 _id 字段设置为 Number 类型,例如:
直接在 Schema 中设置 _id: Number 仅仅是定义了其类型,Mongoose 或 MongoDB 并不会为 Number 类型的 _id 自动生成递增值。若不手动赋值,MongoDB 仍会生成 ObjectId。要实现数字类型的 _id 并使其自动递增,我们需要更精细的控制。
Mongoose 允许我们创建自定义的 SchemaType,这对于验证和规范特定字段的数据类型非常有用。我们将创建一个名为 NumberId 的自定义 SchemaType,用于确保 _id 字段始终是正整数。
const mongoose = require('mongoose');
// 1. 定义自定义 SchemaType 构造函数
function NumberId(key, options) {
// 调用父类 SchemaType 的构造函数
mongoose.SchemaType.call(this, key, options, 'NumberId');
}
// 2. 继承 Mongoose.SchemaType
NumberId.prototype = Object.create(mongoose.SchemaType.prototype);
// 3. 实现 `cast()` 方法进行类型转换和验证
// cast 方法负责将传入的值转换为 SchemaType 所期望的类型
NumberId.prototype.cast = function (val) {
if (typeof val !== 'number') {
throw new Error('NumberId: ' + val + ' 不是一个数字');
}
if (val % 1 !== 0) { // 检查是否为整数
throw new Error('NumberId: ' + val + ' 不是一个整数');
}
if (val < 0) { // 检查是否为正数
throw new Error('NumberId: ' + val + ' 是一个负数');
}
return val;
};
// 4. 注册自定义 SchemaType
mongoose.Schema.Types.NumberId = NumberId;代码解释:
MarketingBlocks AI
AI营销助理,快速创建所有的营销物料。
27
查看详情
注册了自定义 SchemaType 后,我们就可以在模型 Schema 中将其应用于 _id 字段。
// 创建一个新 Schema,使用我们自定义的 SchemaType
const mySchema = new mongoose.Schema(
{
_id: { type: mongoose.Schema.Types.NumberId, required: true, unique: true },
name: String, // 其他字段
// ...
},
{ autoIndex: true } // 建议保留 autoIndex 为 true,除非你手动管理索引
);
// 创建一个模型
const MyModel = mongoose.model('MyModel', mySchema);代码解释:
至此,我们已经成功地将 _id 字段约束为正整数类型。然而,这并没有解决自动生成递增 _id 的问题。如果此时不手动为 _id 赋值,Mongoose 仍然会生成 ObjectId。
Mongoose 或 MongoDB 本身并没有为 Number 类型的 _id 提供内置的自动递增机制(如 SQL 数据库的 AUTO_INCREMENT)。要实现这一点,我们需要采用一种常见的模式:使用一个单独的计数器集合,并在主模型的保存操作前通过钩子(Pre-s*e Hook)来获取并分配递增的 _id。
解决方案:使用单独的计数器集合和预保存钩子
创建计数器模型 (Counter Model): 我们将创建一个简单的 Counter 模型,用于存储每个集合的下一个可用序列号。
// 计数器 Schema
const CounterSchema = new mongoose.Schema({
_id: { type: String, required: true }, // 集合名称,例如 'my_model_id'
sequence_value: { type: Number, default: 0 }
});
const Counter = mongoose.model('Counter', CounterSchema);实现预保存钩子 (Pre-s*e Hook): 在主模型 MyModel 的 pre('s*e') 钩子中,我们将实现逻辑来:
// 修改 MyModel Schema,添加预保存钩子
mySchema.pre('s*e', async function(next) {
const doc = this;
// 仅当 _id 未设置时(即新文档)才执行自动递增
if (doc.isNew && !doc._id) {
try {
const counter = await Counter.findByIdAndUpdate(
{ _id: 'my_model_id' }, // 这里的 _id 对应计数器集合中的文档 ID,通常是模型名称或自定义标识
{ $inc: { sequence_value: 1 } },
{ new: true, upsert: true } // new: 返回更新后的文档;upsert: 如果不存在则创建
);
doc._id = counter.sequence_value;
next();
} catch (error) {
next(error); // 捕获错误并传递给下一个中间件
}
} else {
next(); // 如果 _id 已经存在或不是新文档,则直接跳过
}
});整合完整代码示例:
const mongoose = require('mongoose');
// --- 第一部分:自定义 NumberId SchemaType ---
function NumberId(key, options) {
mongoose.SchemaType.call(this, key, options, 'NumberId');
}
NumberId.prototype = Object.create(mongoose.SchemaType.prototype);
NumberId.prototype.cast = function (val) {
if (typeof val !== 'number') {
throw new Error('NumberId: ' + val + ' 不是一个数字');
}
if (val % 1 !== 0) {
throw new Error('NumberId: ' + val + ' 不是一个整数');
}
if (val < 0) {
throw new Error('NumberId: ' + val + ' 是一个负数');
}
return val;
};
mongoose.Schema.Types.NumberId = NumberId;
// --- 第二部分:计数器模型 ---
const CounterSchema = new mongoose.Schema({
_id: { type: String, required: true }, // 例如 'my_model_id'
sequence_value: { type: Number, default: 0 }
});
const Counter = mongoose.model('Counter', CounterSchema);
// --- 第三部分:主模型 Schema 及预保存钩子 ---
const mySchema = new mongoose.Schema(
{
_id: { type: mongoose.Schema.Types.NumberId, required: true, unique: true },
name: { type: String, required: true },
// 其他字段
},
{ autoIndex: true }
);
// 定义预保存钩子,实现自动递增 _id
mySchema.pre('s*e', async function(next) {
const doc = this;
// 只有当文档是新建且 _id 未手动设置时才自动生成
if (doc.isNew && doc._id === undefined) {
try {
// 查找并原子性递增计数器
const counter = await Counter.findByIdAndUpdate(
{ _id: 'my_model_id' }, // 使用一个固定的 ID 来标识这个模型的计数器
{ $inc: { sequence_value: 1 } },
{ new: true, upsert: true, setDefaultsOnInsert: true } // upsert: 不存在则创建
);
doc._id = counter.sequence_value;
next();
} catch (error) {
console.error('生成 _id 失败:', error);
next(error); // 传递错误
}
} else {
next(); // 如果 _id 已存在或不是新文档,则跳过自动生成
}
});
const MyModel = mongoose.model('MyModel', mySchema);
// --- 使用示例 ---
const mongoUri = 'mongodb://localhost:27017/testdb'; // 请替换为你的 MongoDB URI
async function run() {
await mongoose.connect(mongoUri, {
useNewUrlParser: true,
useUnifiedTopology: true,
});
console.log('MongoDB 连接成功');
// 清空集合以便测试
await MyModel.deleteMany({});
await Counter.deleteMany({}); // 清空计数器
// 确保计数器初始化(如果 upsert: true,则首次保存会自动创建并初始化为1)
// 或者可以手动初始化:await Counter.create({ _id: 'my_model_id', sequence_value: 0 });
try {
// 创建一些文档,_id 将自动生成
const doc1 = await MyModel.create({ name: 'Document One' });
console.log('创建文档 1:', doc1); // _id 应该是 1
const doc2 = await MyModel.create({ name: 'Document Two' });
console.log('创建文档 2:', doc2); // _id 应该是 2
const doc3 = await MyModel.create({ name: 'Document Three' });
console.log('创建文档 3:', doc3); // _id 应该是 3
// 尝试手动设置 _id (如果符合 NumberId 规则,则会使用手动设置的值)
const doc4 = await MyModel.create({ _id: 100, name: 'Document Hundred' });
console.log('创建文档 4 (手动 _id):', doc4); // _id 应该是 100
// 尝试创建无效 _id (非数字或负数)
// await MyModel.create({ _id: 'abc', name: 'Invalid ID' }); // 会抛出 NumberId cast 错误
// await MyModel.create({ _id: -5, name: 'Invalid ID' }); // 会抛出 NumberId cast 错误
} catch (err) {
console.error('操作失败:', err.message);
} finally {
await mongoose.disconnect();
console.log('MongoDB 连接关闭');
}
}
run();代码解释:
本文提供了一个全面的教程,指导您如何在 Mongoose 中将 _id 字段从默认的 ObjectId 更改为 Number 类型,并实现自动递增的序列号。我们首先通过自定义 NumberId SchemaType 确保 _id 字段的类型和值约束,然后通过一个独立的计数器模型和 Mongoose 的 pre('s*e') 钩子,实现了 _id 的原子性自动递增。在实际应用中,请务必根据项目的具体需求和性能考量,权衡选择最合适的 _id 生成策略。
以上就是自定义 Mongoose _id 为数字类型并实现自动递增的详细内容,更多请关注其它相关文章!
# 是一个
# seo优化认证证书
# seo岗位详解
# 建站模型seo
# 广东周边网站建设
# 杭州抖音查看搜索关键词排名
# 门户网站发文推广
# 城口seo推广网站
# 网站建设词汇分析报告
# 怎么做营销抖音账号推广
# 网站建设相关的工作
# 首次
# 这是
# go
# 应该是
# 不存在
# 抛出
# 创建一个
# 自动生成
# 文档
# 自定义
# red
# ai
# 字节
# mongodb
相关栏目:
【
Google疑问12 】
【
Facebook疑问10 】
【
优化推广96088 】
【
技术知识133117 】
【
IDC资讯59369 】
【
网络运营7196 】
【
IT资讯61894 】
相关推荐:
毒蘑菇VOLUMESHADER_BM官网首页登录入口 毒蘑菇VOLUMESHADER_BM官网首页登录入口说明
植物大战僵尸95版游戏版下载_植物大战僵尸95版游戏版安装指南
睡觉时心跳快是什么原因 夜间心悸如何应对
OPPO手机参数配置如何开启护眼模式_OPPO手机参数配置护眼模式开启指南
MySQL多重关联查询:利用别名高效获取同一表的多个关联字段
J*aScript模拟悬停与点击:自动化网页动态元素交互指南
HTML Canvas文本样式定制指南:解决外部字体加载与应用难题
AO3中文版手机快速通道_AO3最新稳定链接更新
《火花chat》搜索好友方法
J*aScript 数值去小数位处理:多种方法与实践
Python对象引用与属性赋值:理解链表中的行为
cad视图选项卡不见了怎么办_cad视图标签恢复显示方法
《崩坏:星穹铁道》3.6版本异相仲裁打法及配队推荐
在Peewee中处理PostgreSQL记录重复:一站式数据摄取教程
C++ priority_queue怎么用_C++优先队列底层实现与自定义比较器
vivo手机视频通话美颜怎么设置_vivo视频通话美颜开启方法
荣耀magicv5怎么上手测评
百度网盘如何设置上传限额
动漫之家观看全集库 动漫之家免费资源网地址
C#解析并修改XML后保存 如何确保格式与编码的正确性
word文档中的分隔符有哪些不同类型和用途_Word分隔符类型与用途方法
mysql归档数据怎么导出为csv_mysql归档数据导出为csv文件的方法
Go Goroutine调度与并发执行深度解析
TikTok笔记文字无法编辑如何解决 TikTok笔记文字编辑优化方法
火狐浏览器如何刷新修复浏览器 火狐浏览器“重置Firefox”功能详解
《三国:谋定天下》平民全阶段通用阵容
微信步数怎么刷_微信步数快速提升技巧
如何在解析前预检查XML文件的完整性? 比如检查文件大小或特定结束标签
Pandas中基于动态偏移量实现DataFrame列值位移的策略
谷歌邮箱怎么换绑定邮箱Gmail安全备份邮箱修改方法
苹果电脑如何快速截图并编辑 苹果电脑截屏标注快捷操作
Flexbox布局中Stencil组件宽度不显示问题解析与:host尺寸控制
苹果SE如何开启单手模式_苹果SE单手操作功能
纯CSS实现滚动时动态时间轴线条颜色填充效果
Sublime Text怎么关闭自动完成_Sublime禁用Auto Complete设置
Python项目中的条件导入:解决跨模块依赖问题
NumPy 高性能技巧:基于多列条件查找最近邻行索引的向量化实现
可米酷漫画在线阅读入口_ 可米酷漫画官网直达链接
顺丰官方查单号入口 顺丰快递单号查询官网入口
wps文字怎么设置文字环绕图片的方式_wps文字如何设置文字环绕图片方式
六级准考证号怎么查_四六级准考证查询入口官网
C++ bind函数使用教程_C++参数绑定与函数适配器的应用
解决Flex容器横向滚动内容截断与偏移问题
tiktok国际版入口_tiktok官网网页版链接
《漫蛙manwa2》防走失网页版链接2025
Flexbox布局:实现粘性导航与底部页脚的完美结合
Excel如何制作月度销售统计图_Excel动态图表制作与控件应用
苹果自助维修计划支持哪些设备机型
向往的生活小游戏启动处_向往的生活小游戏立即启动
使用TinyButStrong生成HTML并结合Dompdf创建PDF教程
2025-11-12
运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。