自定义 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.SchemaTypeNumberId.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. 注册自定义 SchemaTypemongoose.Schema.Types.NumberId = NumberId;

代码解释:

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,使用我们自定义的 SchemaTypeconst 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-save Hook)来获取并分配递增的 _id。

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

创建计数器模型 (Counter Model):我们将创建一个简单的 Counter 模型,用于存储每个集合的下一个可用序列号。

// 计数器 Schemaconst 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-save Hook):在主模型 MyModel 的 pre(‘save’) 钩子中,我们将实现逻辑来:

查找对应集合的计数器文档。原子性地递增计数器值。将递增后的值赋给当前文档的 _id 字段。

// 修改 MyModel Schema,添加预保存钩子mySchema.pre('save', 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 });// 定义预保存钩子,实现自动递增 _idmySchema.pre('save', 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 URIasync 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)。

注意事项与最佳实践

原子性保证: 使用 $inc 操作是实现原子性递增的关键。在高并发环境下,多个保存操作同时进行时,$inc 能够确保每个文档获得一个唯一的、递增的 _id,避免竞态条件。性能考量: 计数器集合的单个文档(如 ‘my_model_id’)会成为所有 MyModel 文档写入操作的瓶颈。在高写入吞吐量的应用中,这可能会影响性能。对于极高并发的场景,可能需要重新评估这种设计,或者考虑其他分布式计数器方案。初始化计数器: 首次使用时,如果计数器文档不存在,upsert: true 会自动创建并初始化 sequence_value。如果需要从特定数字开始,可以在应用启动时手动创建或更新计数器文档。错误处理: 在 pre(‘save’) 钩子中,务必捕获 findByIdAndUpdate 可能抛出的错误,并通过 next(error) 传递出去,以防止应用崩溃或数据不一致。手动指定 _id: 我们的 pre(‘save’) 钩子仅在 doc.isNew && doc._id === undefined 时触发。这意味着如果你在创建文档时手动提供了 _id(例如 await MyModel.create({ _id: 100, name: ‘Custom ID’ })),只要这个 _id 符合 NumberId 的验证规则(正整数),它就会被接受,而不会触发自动递增。替代方案:使用 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(‘save’) 钩子,实现了 _id 的原子性自动递增。在实际应用中,请务必根据项目的具体需求和性能考量,权衡选择最合适的 _id 生成策略。

以上就是自定义 Mongoose _id 为数字类型并实现自动递增的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1538518.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2025年12月21日 05:04:13
下一篇 2025年12月21日 05:04:23

相关推荐

  • jsonp解决跨域问题

    JSONP通过script标签跨域请求数据,利用服务器返回函数调用传递结果,仅支持GET、缺乏错误处理且存在安全风险,现代开发推荐CORS或代理方案。 跨域问题在前端开发中很常见,尤其是请求不同域名下的接口时。JSONP 是早期解决跨域问题的一种方案,利用了 标签不受同源策略限制的特性。 JSONP…

    2025年12月21日
    000
  • Mongoose中将_id字段设置为数字类型并实现自动递增序列号

    本教程详细阐述如何在Mongoose中自定义_id字段的数据类型为数字,而非默认的ObjectId。我们将通过创建自定义SchemaType来强制_id为正整数,并进一步介绍如何结合Mongoose的生命周期钩子和独立的计数器集合,为数字_id字段实现可靠的自动递增序列号生成机制,确保数据一致性和唯…

    2025年12月21日
    000
  • JavaScript JIT编译原理

    JavaScript引擎通过JIT技术边运行边优化,先解析代码为AST,再生成字节码由解释器执行,同时收集类型反馈;当函数被频繁调用成为热点代码时,触发Baseline编译器进行低级优化,随后TurboFan根据类型推测进行深度优化,如内联和冗余消除;若类型假设失败则触发去优化回退到解释执行;内联缓…

    2025年12月21日
    000
  • Mongoose _id字段自定义为Number类型:实现与验证

    本教程详细指导如何在mongoose中将`_id`字段自定义为`number`类型,并实现严格的正整数验证。通过创建自定义schematype,确保`_id`的数据完整性。同时,文章将深入探讨mongodb/mongoose环境下`_id`字段自增长的实现策略,指出仅定义类型无法自动生成序列号,需要…

    2025年12月21日
    000
  • JavaScript 测试驱动:Jest 单元测试编写与 mock 技巧

    本文介绍使用Jest进行JavaScript单元测试,涵盖基础测试、mock函数、模块模拟及高级技巧;2. 通过示例展示如何用expect、jest.fn()、jest.mock()和jest.spyOn隔离依赖并验证行为;3. 强调测试应关注行为而非实现,建议合理使用mock并清理状态以确保可靠性…

    2025年12月21日
    000
  • 保持滚动条在底部:动态内容界面的实现策略

    本文详细介绍了如何在Web应用中,尤其是在处理动态加载内容时,通过JavaScript确保滚动条始终保持在最底部。我们将重点探讨使用`MutationObserver` API来监听DOM变化,并结合`scrollTop`属性实现这一功能,从而优化用户体验,适用于聊天窗口、日志显示或实时数据流等场景…

    2025年12月21日
    000
  • 前端应用中沙盒与生产环境的动态切换与API管理教程

    本教程旨在指导开发者如何在前端应用中实现沙盒(Sandbox)与生产(Production)环境的动态切换。通过构建一个集中式的环境配置管理模块,结合UI交互事件,并抽象API调用层,我们将展示如何允许用户在不同环境间无缝切换,并自动调用相应的API端点,从而显著提升开发、测试和运维的灵活性与效率。…

    2025年12月21日
    000
  • 在React应用中实现沙盒与生产环境的动态切换与API管理

    本教程详细介绍了如何在React应用中构建一个健壮的环境切换机制,以动态管理沙盒(Sandbox)与生产(Production)模式。内容涵盖了如何通过集中式配置定义不同环境的API端点,实现UI界面的实时更新,以及利用API抽象层确保API请求根据当前环境自动路由,从而提升应用的可维护性和开发效率…

    2025年12月21日
    000
  • JS对象创建怎么实现_JS对象创建与属性方法使用教程

    对象字面量创建简洁但难复用;2. 构造函数可批量创建但方法重复;3. 原型共享方法节省内存;4. ES6 class语法清晰推荐使用;5. 可动态增删属性方法,灵活操作。 JavaScript 中创建对象是开发中的基础操作,掌握多种对象创建方式和属性方法的使用,能帮助你写出更清晰、可维护的代码。下面…

    2025年12月21日
    000
  • 实现前端应用沙盒与生产环境动态切换及API管理

    本教程详细阐述了如何在前端应用中实现沙盒(sandbox)与生产(production)环境的动态切换。通过构建集中的环境配置管理模块和抽象化的api服务层,开发者可以轻松地根据用户操作或运行时环境切换不同的api端点及相关配置,从而提高开发效率和应用灵活性。 在现代Web应用开发中,区分不同运行环…

    2025年12月21日
    000
  • React Router DOM 导航状态传递复杂对象:解决方案与最佳实践

    本文旨在解决使用 React Router DOM 的 `navigate` 方法传递复杂对象时,目标%ignore_a_1%无法正确接收状态数据的问题。核心在于理解 `history.pushState` 对数据类型的限制,并提供通过 JSON 序列化/反序列化来确保复杂对象(如用户对象)能够成功…

    2025年12月21日
    000
  • 自动滚动至容器底部:利用 MutationObserver 管理动态内容滚动

    本文深入探讨了如何利用 JavaScript 的 `MutationObserver` API,实现对动态内容容器(如自定义下拉菜单、聊天窗口或日志输出)的自动滚动管理。我们将学习如何监听 DOM 元素的子节点变化,并在内容更新时自动将滚动条定位到容器底部,确保用户始终能看到最新内容。文章将提供详细…

    2025年12月21日
    000
  • 在Express应用中为Firestore文档生成自定义递增ID的教程

    本教程将指导您如何在Express后端应用中为Firestore文档生成自定义的、具有特定格式的递增ID,而不是依赖Firestore的自动生成ID或使用现有字段。我们将通过维护一个计数器文档并利用Firestore事务来确保ID生成的唯一性和原子性,同时提供具体的代码实现和注意事项。 理解Fire…

    2025年12月21日
    000
  • Node.js Express 路由聚合:内部逻辑复用与高效数据整合

    本教程详细阐述了在 Node.js Express 应用中,如何在一个主路由端点内部高效地聚合和调用多个子路由的业务逻辑,避免不必要的 HTTP 请求或子进程开销。通过将核心业务逻辑抽象为可复用的函数,并结合异步编程模式,实现代码的解耦、性能优化和更高的可维护性,从而构建更健壮、响应更快的 API …

    2025年12月21日
    000
  • 前端应用中沙盒与生产环境切换及API动态管理教程

    本教程旨在指导开发者如何在前端应用中实现沙盒(Sandbox)与生产(Production)模式的动态切换,并根据当前模式自动调整API请求的URL。通过构建一个集中式的环境配置模块和一个抽象化的API服务类,我们将实现视图和后端接口的无缝切换,提升开发效率和应用的可维护性。 在现代前端应用的开发过…

    2025年12月21日
    000
  • MongoDB聚合:实现日期差异的精确向下取整(非$dateDiff默认行为)

    在mongodb聚合管道中,原生的`$datediff`操作符在计算日期差异时,对于非整数结果会默认进行四舍五入。当需要严格的向下取整(floor)行为时,例如将2小时54分钟计为2小时而非3小时,可以通过结合使用`$subtract`计算毫秒差、`$divide`转换为目标单位,最后应用`$flo…

    2025年12月21日
    000
  • JavaScript对象数组列值完整性校验:避免空值不一致问题

    本教程旨在提供一种高效且易读的javascript方法,用于校验复杂对象数组中特定列的数据一致性。通过利用`object.keys`、`map`和`every`等高阶函数,您可以优雅地确保数组中所有对象对于某个属性而言,要么全部拥有非空值,要么全部为空值,从而避免数据不完整或不一致的问题。 引言 在…

    2025年12月21日
    000
  • MongoDB聚合管道:实现日期时间差的向下取整(Floor)计算

    本教程将深入探讨在mongodb聚合管道中如何精确计算两个日期之间的差异,并对结果进行向下取整(floor)操作。针对 `$datediff` 操作符可能不满足特定向下取整需求的情况,文章将详细介绍一种利用 `$subtract` 获取毫秒差并结合 `$floor` 函数实现自定义时间单位(如小时)…

    2025年12月21日
    000
  • AJAX数据解析:解决JSON中嵌套JSON字符串的访问问题

    本文探讨了ajax请求返回的json数据中,某个字段值实际上是另一个json结构字符串的常见问题。文章解释了为何直接访问此类嵌套属性会导致`undefined`,并提供了明确的解决方案:通过`json.parse()`方法对嵌套的json字符串进行二次解析,将其转换为可操作的javascript对象…

    2025年12月21日
    000
  • 怎样开发一个验证码生成插件_JavaScript验证码插件功能与安全实现方法

    验证码插件通过Canvas生成带干扰元素的随机字符图像,支持刷新与自定义配置,前端仅用于交互展示,真实校验须由后端完成以确保安全。 开发一个验证码生成插件,核心目标是实现简单易用、可定制性强,并具备基本的安全防护能力。JavaScript 验证码插件通常用于前端表单验证,防止机器人自动提交,虽然不能…

    2025年12月21日
    000

发表回复

登录后才能评论
关注微信