新闻中心

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

2025-11-12
浏览次数:
返回列表

自定义 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;

代码解释:

企业黄页模块 for PHPCMS9 GBK 正式版 企业黄页模块 for PHPCMS9 GBK 正式版

PHPCMS V9采用OOP(面向对象)方式进行基础运行框架搭建。模块化开发方式做为功能开发形式。框架易于功能扩展,代码维护,优秀的二次开发能力,可满足所有网站的应用需求。 PHPCMS V9企业黄页主要特色1、模型自定义,支持模型添加、修改、删除、导出、导入功能;2、模型字段自定义,支持模型字段添加、修改、删除、禁用操作;3、分类无限添加,支持批量多级添加;4、新增附件字段功能,实现相同模型,不

企业黄页模块 for PHPCMS9 GBK 正式版 0 查看详情 企业黄页模块 for PHPCMS9 GBK 正式版
  • 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 为数字类型并实现自动递增的详细内容,更多请关注其它相关文章!


# 应该是  # 揭阳网站建设营销中心  # 营销线上推广预算表  # 给餐饮店做推广营销  # 嘉峪关品牌网站建设招标  # 临夏回族做网站推广  # 东昌府网站建设推广  # 时代云来营销推广  # qq空间seo优化  # 转转营销推广服务到期  # 七七seo虾哥网络  # 这是  # 是一个  # go  # 不存在  # 抛出  # 企业黄页  # 创建一个  # 自动生成  # 文档  # 自定义  # red  # ai  # 字节  # mongodb 


相关栏目: 【 科技资讯46185 】 【 网络学院92790


相关推荐: MAC的“快捷指令”怎么同步到iPhone_MAC利用iCloud同步所有设备的自动化指令  UC浏览器官网入口2025最新 UC浏览器网页版正式地址  狙击外星人小游戏开始_狙击外星人小游戏立即开始  一加手机电池耗电快怎么办_一加手机电池耗电快的解决方法  J*aScript实现单选按钮与关联输入框的联动禁用教程  Typer应用中灵活处理命令行参数的令牌化与解析  Win10磁盘清理工具在哪 Win10打开并使用磁盘清理【教程】  2026春节假期票务安排_2026春节放假购票指南  文心一言怎样用插件调度API数据_文心一言用插件调度API数据【API调用】  如何创建没有密码的Windows本地账户_跳过微软账户登录的技巧【教程】  Sublime Text怎么显示空格和制表符_Sublime显示不可见字符设置  CSS Flexbox如何实现多行排列_flex-wrap wrap自动换行显示  解决 Express.js 中 PUT 请求密码修改失败的路由配置指南  凉拌黄瓜怎么拌更入味 凉拌黄瓜简单家常做法  CSS自定义字体样式被系统字体替换怎么办_font-face方式指定font-display控制渲染策略  Go语言HTML解析:利用Goquery精准获取指定元素内容  押井守高度称赞《辐射4》:玩了八年都停不下来!  PHP URL参数传递与500错误调试指南  Mac怎么锁定备忘录_Mac备忘录加密设置教程  QQ邮箱官网登录入口 QQ邮箱网页版邮箱快速登录  Python类型检查:优化关联可选属性的Mypy推断策略  sublime如何配置Python开发环境_将sublime打造成轻量级Python IDE  “在文档元素之后找到了标记”是什么错误? 检查并修复XML中多个根元素的3个方法  QQ邮箱网页版入口 QQ邮箱官方邮箱登录通道  解决Python logging 中 datefmt 导致时间戳固定不变的问题  《燕云十六声》两周内达九百万玩家!位居畅销榜第五  C++ string find函数返回值npos详解_C++字符串查找失败的判断条件  J*aScript数组对象转换:按指定键分组与值收集  HTML空白字符处理机制:渲染、DOM与编码实践  QQ邮箱在线使用入口 QQ邮箱个人账号网页版登录  2025AO3夸克浏览器通道_AO3手机HTTPS安全入口分享  Surface怎么安装系统 微软Surface Pro U盘重装win11教程  学习通在线学习平台 学习通网页版直接进入课程中心  解决 MongoDB 聚合查询中对象数组 _id 匹配问题  三星GalaxyZFold5怎样在相册制作折叠屏分镜_iPhone三星GalaxyZFold5相册制作折叠屏分镜【创意编辑】  《明末:渊虚之羽》设计师谈设计角色:那会刚毕业 充满激情  Golang如何实现简单的Web表单_Golang表单提交与验证处理方法  Win11输入法不见了怎么办_Windows11恢复语言栏显示方法  win11开机启动修复循环怎么办 Win11无法进入系统高级启动解决方法【修复】  深入理解J*a编译器的兼容性选项:从-source到--release  处理动态列数据:J*a ArrayList的正确初始化与字符累加教程  Win11怎么合并任务栏图标 Win11开启任务栏合并减少图标占空间【方法】  sublime如何处理大型CSV文件的列对齐_sublime高级表格编辑插件指南  如何有效阻止外部脚本意外修改内联样式的高度属性  微博网页版官方账号登录 微博网页版内容浏览使用指南  漫蛙manwa官网登录界面_漫蛙漫画网页版主站入口  一加Ace 6T实拍样张首次公布!李杰:主摄实力完全看齐4K档性能旗舰  sublime如何配置Go语言开发环境_sublime搭建Golang编译运行系统  CSS布局:解决全屏元素100%尺寸与外边距导致的页面溢出问题  J*aScript中如何高效提取对象指定属性 

搜索