自定义 Mongoose _id 为数字类型并实现自动递增


自定义 mongoose _id 为数字类型并实现自动递增

本文详细介绍了如何在 Mongoose 中将 `_id` 字段的数据类型从默认的 `ObjectId` 更改为 `Number`,并实现自动递增的序列号。我们将通过创建自定义 SchemaType 来验证数字 `_id`,并进一步结合预保存钩子和单独的计数器集合,实现 `_id` 字段的原子性自动生成,提供完整的代码示例和最佳实践。

引言:Mongoose _id 字段的自定义需求

Mongoose 模型默认使用 ObjectId 作为文档的 _id 字段,它是一个 12 字节的唯一标识符,通常能满足大多数应用的需求。然而,在某些特定场景下,开发者可能希望将 _id 字段设置为 Number 类型,例如:

  • 为了提高可读性,希望 _id 是简单的整数序列。
  • 与现有关系型数据库系统集成时,需要匹配其自增主键。
  • 特定的业务逻辑要求 _id 具有数字顺序。

直接在 Schema 中设置 _id: Number 仅仅是定义了其类型,Mongoose 或 MongoDB 并不会为 Number 类型的 _id 自动生成递增值。若不手动赋值,MongoDB 仍会生成 ObjectId。要实现数字类型的 _id 并使其自动递增,我们需要更精细的控制。

第一步:定义自定义 NumberId SchemaType

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 MarketingBlocks AI

AI营销助理,快速创建所有的营销物料。

MarketingBlocks AI 27 查看详情 MarketingBlocks AI
  • NumberId(key, options): 自定义 SchemaType 的构造函数,它会调用 mongoose.SchemaType 的构造函数,并指定类型名称为 'NumberId'。
  • NumberId.prototype = Object.create(mongoose.SchemaType.prototype): 使得 NumberId 继承自 mongoose.SchemaType,从而拥有 Mongoose SchemaType 的基本功能。
  • cast(val) 方法:这是自定义 SchemaType 的核心。当 Mongoose 尝试将一个值赋给 NumberId 类型的字段时,会调用此方法。我们在此方法中实现了严格的验证逻辑:
    • 检查值是否为 number 类型。
    • 检查值是否为整数(val % 1 !== 0)。
    • 检查值是否为正数(val
    • 如果验证通过,返回原始值;否则,抛出错误。
  • mongoose.Schema.Types.NumberId = NumberId: 将我们自定义的 NumberId 注册到 Mongoose 的 SchemaType 集合中,这样我们就可以在 Schema 定义中通过 mongoose.Schema.Types.NumberId 来引用它。

第二步:在 Mongoose Schema 中使用自定义 NumberId

注册了自定义 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: { type: mongoose.Schema.Types.NumberId, required: true, unique: true }: 这里我们将 _id 字段的类型指定为我们自定义的 NumberId。同时,required: true 确保 _id 必须存在,unique: true 确保 _id 在集合中是唯一的。
  • autoIndex: true: 尽管 _id 字段通常由 MongoDB 自动索引,但保留此选项可以确保其他自定义索引也能被自动创建。

至此,我们已经成功地将 _id 字段约束为正整数类型。然而,这并没有解决自动生成递增 _id 的问题。如果此时不手动为 _id 赋值,Mongoose 仍然会生成 ObjectId。

第三步:实现数字 _id 的自动递增(核心挑战与解决方案)

Mongoose 或 MongoDB 本身并没有为 Number 类型的 _id 提供内置的自动递增机制(如 SQL 数据库的 AUTO_INCREMENT)。要实现这一点,我们需要采用一种常见的模式:使用一个单独的计数器集合,并在主模型的保存操作前通过钩子(Pre-s*e Hook)来获取并分配递增的 _id。

解决方案:使用单独的计数器集合和预保存钩子

  1. 创建计数器模型 (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);
  2. 实现预保存钩子 (Pre-s*e Hook): 在主模型 MyModel 的 pre('s*e') 钩子中,我们将实现逻辑来:

    • 查找对应集合的计数器文档。
    • 原子性地递增计数器值。
    • 将递增后的值赋给当前文档的 _id 字段。
    // 修改 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();

代码解释:

  • doc.isNew && doc._id === undefined: 这个条件确保只有当文档是新建的,并且 _id 字段没有被手动赋值时,才触发自动递增逻辑。这样,如果你想手动指定 _id,也可以实现。
  • Counter.findByIdAndUpdate({ _id: 'my_model_id' }, { $inc: { sequence_value: 1 } }, { new: true, upsert: true, setDefaultsOnInsert: true }):
    • _id: 'my_model_id':这是计数器文档的 _id,用于唯一标识 MyModel 的序列号。你可以根据需要为不同的模型设置不同的计数器 ID。
    • $inc: { sequence_value: 1 }: MongoDB 的原子操作,用于将 sequence_value 字段递增 1。这保证了在高并发下 _id 的唯一性和顺序性。
    • new: true: 返回更新后的文档。
    • upsert: true: 如果没有找到 _id 为 'my_model_id' 的计数器文档,则创建一个新的。
    • setDefaultsOnInsert: true: 当 upsert: true 且创建新文档时,应用 Schema 中定义的默认值(这里 sequence_value 默认为 0)。
  • doc._id = counter.sequence_value: 将获取到的递增值赋给当前文档的 _id。
  • next(): 调用 next() 将控制权传递给下一个中间件或保存操作。如果发生错误,应调用 next(error)。

注意事项与最佳实践

  1. 原子性保证: 使用 $inc 操作是实现原子性递增的关键。在高并发环境下,多个保存操作同时进行时,$inc 能够确保每个文档获得一个唯一的、递增的 _id,避免竞态条件。
  2. 性能考量: 计数器集合的单个文档(如 'my_model_id')会成为所有 MyModel 文档写入操作的瓶颈。在高写入吞吐量的应用中,这可能会影响性能。对于极高并发的场景,可能需要重新评估这种设计,或者考虑其他分布式计数器方案。
  3. 初始化计数器: 首次使用时,如果计数器文档不存在,upsert: true 会自动创建并初始化 sequence_value。如果需要从特定数字开始,可以在应用启动时手动创建或更新计数器文档。
  4. 错误处理: 在 pre('s*e') 钩子中,务必捕获 findByIdAndUpdate 可能抛出的错误,并通过 next(error) 传递出去,以防止应用崩溃或数据不一致。
  5. 手动指定 _id: 我们的 pre('s*e') 钩子仅在 doc.isNew && doc._id === undefined 时触发。这意味着如果你在创建文档时手动提供了 _id(例如 await MyModel.create({ _id: 100, name: 'Custom ID' })),只要这个 _id 符合 NumberId 的验证规则(正整数),它就会被接受,而不会触发自动递增。
  6. 替代方案:
    • 使用 ObjectId 作为 _id,另加 serialNumber 字段: 如果 _id 的类型和性能要求不那么严格,可以继续使用 ObjectId 作为 _id,然后添加一个名为 serialNumber 的 Number 字段,并用上述计数器方法为其自动生成递增值。这样 _id 保持分布式生成,而 serialNumber 提供业务上的序数。
    • UUID/ULID: 如果只需要唯一性而非连续性,可以考虑使用 UUID 或 ULID。它们也是字符串,但比 ObjectId 更短,且可以按时间排序(ULID)。

总结

本文提供了一个全面的教程,指导您如何在 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

了解您产品搜索量及市场趋势,制定营销计划

同行竞争及网站分析保障您的广告效果

点击免费数据支持

提交您的需求,1小时内享受我们的专业解答。

运城市盐湖区信雨科技有限公司


运城市盐湖区信雨科技有限公司

运城市盐湖区信雨科技有限公司是一家深耕海外推广领域十年的专业服务商,作为谷歌推广与Facebook广告全球合作伙伴,聚焦外贸企业出海痛点,以数字化营销为核心,提供一站式海外营销解决方案。公司凭借十年行业沉淀与平台官方资源加持,打破传统外贸获客壁垒,助力企业高效开拓全球市场,成为中小企业出海的可靠合作伙伴。

 8156699

 13765294890

 8156699@qq.com

Notice

We and selected third parties use cookies or similar technologies for technical purposes and, with your consent, for other purposes as specified in the cookie policy.
You can consent to the use of such technologies by closing this notice, by interacting with any link or button outside of this notice or by continuing to browse otherwise.