
本教程详细指导如何在mongoose中将`_id`字段自定义为`number`类型,并实现严格的正整数验证。通过创建自定义schematype,确保`_id`的数据完整性。同时,文章将深入探讨mongodb/mongoose环境下`_id`字段自增长的实现策略,指出仅定义类型无法自动生成序列号,需要额外的业务逻辑支持,例如利用独立的计数器集合。
引言
Mongoose默认使用ObjectId作为文档的_id字段类型,它是一个12字节的唯一标识符。然而,在某些业务场景下,开发者可能希望将_id设置为Number类型,并且期望它能像关系型数据库的主键一样自动递增。直接在Schema中定义_id: Number并不能实现自动递增的功能,它只会将_id字段的类型限制为数字,但仍会由mongodb生成ObjectId,或者要求用户手动提供一个数字。为了实现_id为数字类型并进行有效验证,我们需要创建自定义的Mongoose SchemaType。
自定义 NumberId SchemaType
Mongoose允许开发者定义自己的SchemaType,以扩展其内置类型的功能。这里,我们将创建一个名为NumberId的自定义类型,它将强制_id字段为正整数。
- 继承 mongoose.SchemaType: 自定义SchemaType需要继承自mongoose.SchemaType。
- 实现 cast() 方法: cast() 方法是自定义类型中的核心,它负责将输入值转换为期望的类型,并在转换失败或值不符合要求时抛出错误。在这个方法中,我们将验证输入值是否为数字、整数且为非负数。
- 注册新的SchemaType: 将自定义类型注册到mongoose.Schema.Types中,使其可以在Schema定义中被引用。
以下是实现NumberId自定义SchemaType的代码:
const mongoose = require('mongoose'); // 1. 定义自定义SchemaType:NumberId function NumberId(key, options) { // 调用父类构造函数,并指定类型名称为 'NumberId' mongoose.SchemaType.call(this, key, options, 'NumberId'); } // 2. 继承mongoose.SchemaType的原型 NumberId.prototype = Object.create(mongoose.SchemaType.prototype); // 3. 实现cast()方法进行类型转换和验证 NumberId.prototype.cast = function (val) { if (typeof val !== 'number') { throw new Error('NumberId: ' + val + ' is not a number'); } if (val % 1 !== 0) { // 检查是否为整数 throw new Error('NumberId: ' + val + ' is not an integer'); } if (val < 0) { // 检查是否为非负数 throw new Error('NumberId: ' + val + ' is negative'); } return val; }; // 4. 注册新的SchemaType mongoose.Schema.Types.NumberId = NumberId;
上述代码定义了一个名为NumberId的SchemaType,它强制要求字段值必须是正整数。任何不符合这些条件的值都将在保存时抛出错误。
在Schema中应用自定义 NumberId 类型
一旦自定义类型被注册,就可以像使用Mongoose内置类型一样,在Schema定义中引用它。下面是创建一个模型并使用NumberId类型作为_id字段的示例:
const mongoose = require('mongoose'); // ... (此处省略NumberId SchemaType的定义和注册代码,假设已完成) ... // 注册新的SchemaType // mongoose.Schema.Types.NumberId = NumberId; // 确保此行已执行 // 创建一个使用我们自定义NumberId类型的Schema const mySchema = new mongoose.Schema( { _id: { type: mongoose.Schema.Types.NumberId, required: true, unique: true }, name: String, // 示例:其他字段 }, { autoIndex: false // 生产环境建议关闭自动创建索引,以手动管理索引 } ); // 创建模型 const MyModel = mongoose.model('MyModel', mySchema); // 连接MongoDB并进行示例操作 const mongoUri = 'mongodb://localhost:27017/test_db'; async function run() { await mongoose.connect(mongoUri, { useNewUrlParser: true, useUnifiedTopology: true, }); console.log('MongoDB connected.'); try { // 尝试创建文档,_id需要手动提供 await MyModel.create({ _id: 1, name: 'Item One' }); await MyModel.create({ _id: 2, name: 'Item Two' }); await MyModel.create({ _id: 3, name: 'Item Three' }); console.log('Documents created successfully with custom Number _id.'); // 尝试创建无效_id的文档,这些操作将抛出错误 // await MyModel.create({ _id: -1, name: 'Invalid Item' }); // await MyModel.create({ _id: 1.5, name: 'Invalid Item' }); // await MyModel.create({ _id: 'abc', name: 'Invalid Item' }); } catch (error) { console.error('Error creating documents:', error.message); } finally { await mongoose.disconnect(); console.log('MongoDB disconnected.'); } } run();
在上述示例中,_id字段被明确指定为mongoose.Schema.Types.NumberId类型,并且required: true和unique: true确保了其唯一性和存在性。需要注意的是,尽管我们定义了NumberId类型,但在创建文档时,_id的值仍然需要手动提供。
关于 _id 自增长的实现策略
虽然我们已经成功地将_id定义为Number类型并实现了验证,但MongoDB和Mongoose本身并没有提供像关系型数据库那样内置的自动递增Number类型_id机制。如果需要实现_id的自增长,通常需要额外的逻辑。以下是两种常见的实现策略:
-
使用独立的计数器集合 (Counters Collection): 这是MongoDB中实现自增长序列最常见且推荐的方法。
- 创建一个专门的集合(例如counters),其中每个文档存储一个序列名称和对应的当前值。
- 在每次需要生成新_id时,原子性地查找并更新(findOneAndUpdate)该计数器文档,获取递增后的值。
- 将获取到的值赋给新文档的_id。
示例伪代码(结合Mongoose的pre(‘save’)钩子):
// 假设MyModel的Schema定义如上 mySchema.pre('save', async function(next) { // 仅在新文档且_id未手动指定时执行自增长逻辑 if (this.isNew && !this._id) { try { // 原子性地查找并更新计数器 const counter = await mongoose.connection.db.collection('counters').findOneAndUpdate( { _id: 'myModelId' }, // 使用模型名称作为计数器ID { $inc: { sequence_value: 1 } }, // 递增sequence_value { returnDocument: 'after', upsert: true } // 返回更新后的文档,如果不存在则创建 ); this._id = counter.value.sequence_value; // 将递增后的值赋给_id next(); } catch (error) { next(error); // 捕获并传递错误 } } else { next(); // 如果是更新操作或_id已指定,则跳过 } });这种方法确保了在并发环境下的序列号唯一性和正确性。
-
利用外部服务或库: 对于更复杂的分布式系统,可以考虑使用专门的ID生成服务(如UUID、Snowflake算法)或第三方库来生成唯一的数字ID。但请注意,这些方法生成的ID通常不是连续的序列号。
重要提示: 上述自定义NumberId SchemaType主要用于_id的类型验证,确保其为有效的正整数。它本身不提供自增长功能。实现自增长需要结合Mongoose的pre(‘save’)钩子和MongoDB的原子操作来管理序列。
总结与注意事项
本教程展示了如何通过自定义Mongoose SchemaType将_id字段设置为Number类型并