Mongoose SchemaTypes 完全指南:路径类型定义、SchemaType 选项与类型转换实战
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
Mongoose 是运行在异步环境下的 MongoDB 对象建模工具,而 SchemaTypes 正是其数据建模的基石:它为模型中的每一个属性(path)定义类型、默认值、校验规则、getter/setter 与字段选择行为。本文以官方 SchemaTypes 文档 为核心骨架,结合本仓库源码实现,系统讲解什么是 SchemaType、type关键字的特殊语义、全部内置 SchemaType 的配置选项与转换规则,并给出可直接复制运行的实战示例,帮助你彻底掌握 Mongoose 路径类型体系。
什么是 SchemaType?
可以把 Mongoose 的 schema 理解为 Mongoose model 的"配置对象",而 SchemaType 则是单个属性的配置对象。一个 SchemaType 决定了某个 path 应该是什么类型、是否有 getter/setter,以及哪些值对该 path 合法。
const schema = new Schema({ name: String }); schema.path('name') instanceof mongoose.SchemaType; // true schema.path('name') instanceof mongoose.Schema.Types.String; // true schema.path('name').instance; // 'String'关键要区分SchemaType 与类型本身:mongoose.ObjectId !== mongoose.Types.ObjectId。SchemaType 只是给 Mongoose 使用的配置对象,mongoose.ObjectId这个 SchemaType 的实例并不会真正创建 MongoDB ObjectId,它只是 schema 中某个 path 的配置。
从源码看,所有内置 SchemaType 都注册在 lib/schema/index.js 中,它们统一继承自 lib/schemaType.js 这个基类,例如 lib/schema/string.js 中的SchemaString就是通过SchemaType.call(this, key, options, 'String', parentSchema)完成初始化,并带有自己的schemaName、defaultOptions与OptionsConstructor。因此,Mongoose 插件也可以注册自定义 SchemaType 来扩展这个体系。
SchemaTypes 全集
Mongoose 内置的全部合法 SchemaType 如下(插件还可以添加自定义类型,如 int32、mongoose-long 等,可通过 Mongoose 插件站点搜索获取):
- String
- Number
- Date
- Buffer
- Boolean
- Mixed
- Union
- ObjectId
- Array
- Decimal128
- Map
- Schema
- UUID
- BigInt
- Double
- Int32
综合示例
const schema = new Schema({ name: String, binary: Buffer, living: Boolean, updated: { type: Date, default: Date.now }, age: { type: Number, min: 18, max: 65 }, mixed: Schema.Types.Mixed, union: { type: Schema.Types.Union, of: [String, Number] }, _someId: Schema.Types.ObjectId, decimal: Schema.Types.Decimal128, double: Schema.Types.Double, int32bit: Schema.Types.Int32, array: [], ofString: [String], ofNumber: [Number], ofDates: [Date], ofBuffer: [Buffer], ofBoolean: [Boolean], ofMixed: [Schema.Types.Mixed], ofObjectId: [Schema.Types.ObjectId], ofArrays: [[]], ofArrayOfNumbers: [[Number]], nested: { stuff: { type: String, lowercase: true, trim: true } }, map: Map, mapOfString: { type: Map, of: String } }); // example use const Thing = mongoose.model('Thing', schema); const m = new Thing; m.name = 'Statue of Liberty'; m.age = 125; m.updated = new Date; m.binary = Buffer.alloc(0); m.living = false; m.mixed = { any: { thing: 'i want' } }; m.markModified('mixed'); m._someId = new mongoose.Types.ObjectId; m.array.push(1); m.ofString.push('strings!'); m.ofNumber.unshift(1, 2, 3, 4); m.ofDates.addToSet(new Date); m.ofBuffer.pop(); m.ofMixed = [1, [], 'three', { four: 5 }]; m.nested.stuff = 'good'; m.map = new Map([['key', 'value']]); m.save(callback);type关键字的特殊语义
type是 Mongoose schema 中的特殊属性。当 Mongoose 在 schema 中发现名为type的嵌套属性时,会假定你需要用给定类型定义一个 SchemaType:
// 3 个 String SchemaTypes: 'name', 'nested.firstName', 'nested.lastName' const schema = new Schema({ name: { type: String }, nested: { firstName: { type: String }, lastName: { type: String } } });因此,如果你真的想定义一个名为type的属性,就需要多做一些工作。例如构建一个股票持仓应用,想存储资产的type(股票 stock、债券 bond、ETF 等),直观写法可能是:
const holdingSchema = new Schema({ // 你期望 `asset` 是一个拥有 2 个属性的对象, // 但不幸的是 `type` 在 Mongoose 中是特殊的, // 所以 Mongoose 会把该 schema 解释为 `asset` 是一个字符串 asset: { type: String, ticker: String } });当 Mongoose 看到type: String时,会假定你意思是asset应该是字符串,而不是一个带type属性的对象。正确的定义方式如下:
const holdingSchema = new Schema({ asset: { // 变通方法:确保 Mongoose 知道 `asset` 是对象、 // `asset.type` 是字符串,而不是把 `asset` 当作字符串 type: { type: String }, ticker: String } });更多细节可参考仓库 FAQ 文档 中关于type关键字的说明。
SchemaType Options(SchemaType 选项)
你可以直接用类型本身声明 schema type,也可以用带type属性的对象声明:
const schema1 = new Schema({ test: String // `test` 是一个 String 类型的 path }); const schema2 = new Schema({ // `test` 对象包含 "SchemaType options" test: { type: String } // `test` 是一个 string 类型的 path });除了type属性,你还可以为 path 指定附加属性。例如想在保存前把小写化字符串:
const schema2 = new Schema({ test: { type: String, lowercase: true // 总是把 `test` 转换成小写 } });你可以向 SchemaType options 中添加任何自定义属性,很多插件依赖自定义的 SchemaType options(例如 mongoose-autopopulate 插件在 options 中设置autopopulate: true即可自动 populate 路径)。Mongoose 内置支持若干 SchemaType 选项,如上面示例中的lowercase。lowercase只对字符串生效;有些选项对所有 schema type 通用,有些则只对特定类型生效。
所有 SchemaType 通用选项
required: boolean 或 function,若为 true 则为该属性添加 required 校验器default: 任意值或 function,为该 path 设置默认值;如果值是函数,则使用函数返回值作为默认值select: boolean,指定查询的默认投影(projection)行为validate: function,为该属性添加校验函数get: function,使用Object.defineProperty()为该属性定义自定义 getterset: function,使用Object.defineProperty()为该属性定义自定义 setteralias: string(mongoose >= 4.10.0),定义一个虚拟属性(virtual),用来 get/set 该 pathimmutable: boolean,将 path 定义为不可变;除非父文档isNew: true,否则 Mongoose 禁止修改 immutable pathtransform: function,调用Document#toJSON()(包括对文档执行JSON.stringify())时触发
const numberSchema = new Schema({ integerOnly: { type: Number, get: v => Math.round(v), set: v => Math.round(v), alias: 'i' } }); const Number = mongoose.model('Number', numberSchema); const doc = new Number(); doc.integerOnly = 2.001; doc.integerOnly; // 2 doc.i; // 2 doc.i = 3.001; doc.integerOnly; // 3 doc.i; // 3注意immutable的底层实现:仓库 lib/helpers/query/handleImmutable.js 与 lib/helpers/schematype/handleImmutable.js 会在更新操作(update 与文档保存)期间把 immutable path 从修改集合中剥离,从而防止被意外改写,只有新建文档(isNew: true)时允许写入。
索引选项
你还可以用 schema type options 定义 MongoDB 索引:
index: boolean,是否在该属性上定义索引unique: boolean,是否定义唯一索引sparse: boolean,是否定义稀疏索引
const schema2 = new Schema({ test: { type: String, index: true, unique: true // 唯一索引。如果指定 `unique: true` // 再指定 `index: true` 是可选的 } });各类型专属选项
String(详见 validation 文档):
lowercase: boolean,是否总是对值调用.toLowerCase()uppercase: boolean,是否总是对值调用.toUpperCase()trim: boolean,是否总是对值调用.trim()match: RegExp,创建校验器,检查值是否匹配给定的正则表达式enum: Array,创建校验器,检查值是否在给定数组中minLength: Number,创建校验器,检查值长度不小于给定值maxLength: Number,创建校验器,检查值长度不大于给定值populate: Object,设置默认 populate 选项
Number:
min: Number,创建校验器,检查值大于等于给定最小值max: Number,创建校验器,检查值小于等于给定最大值enum: Array,创建校验器,检查值与数组中某个值严格相等populate: Object,设置默认 populate 选项
Date:
min: Date,创建校验器,检查值大于等于给定最小值max: Date,创建校验器,检查值小于等于给定最大值expires: Number 或 String,创建 TTL 索引,值以秒为单位
ObjectId:
populate: Object,设置默认 populate 选项
各 SchemaType 实战指南
String
声明字符串 path,既可以使用String全局构造函数,也可以使用字符串'String':
const schema1 = new Schema({ name: String }); // name 会被 cast 为字符串 const schema2 = new Schema({ name: 'String' }); // 等价 const Person = mongoose.model('Person', schema2);如果传入的元素有toString()函数,Mongoose 会调用它——除非该元素是数组,或者toString()函数与Object.prototype.toString()严格相等:
new Person({ name: 42 }).name; // "42" 作为字符串 new Person({ name: { toString: () => 42 } }).name; // "42" 作为字符串 // "undefined",如果 save() 该文档会得到 cast 错误 new Person({ name: { foo: 42 } }).name;从 lib/schema/string.js 的源码可以看到,SchemaString维护了enumValues与regExp两个实例属性(对应enum和match选项),并支持通过SchemaString.cast(caster)静态方法整体替换 cast 函数,或传入false禁用 cast(仅允许null/undefined和非对象值)。
Number
声明数字 path,可以使用Number全局构造函数或字符串'Number':
const schema1 = new Schema({ age: Number }); // age 会被 cast 为 Number const schema2 = new Schema({ age: 'Number' }); // 等价 const Car = mongoose.model('Car', schema2);下面这些值都能成功 cast 为 Number:
new Car({ age: '15' }).age; // 15 作为 Number new Car({ age: true }).age; // 1 作为 Number new Car({ age: false }).age; // 0 作为 Number new Car({ age: { valueOf: () => 83 } }).age; // 83 作为 Number如果传入的对象带有返回 Number 的valueOf()函数,Mongoose 会调用它并把返回值赋给该 path。null和undefined不会被 cast。NaN、能 cast 成 NaN 的字符串、数组,以及没有valueOf()函数的对象,都只会在验证阶段抛出 CastError——即初始化时不抛错,只有验证时才抛错。
Dates
内置的 Date 方法 并没有被接入 Mongoose 的变更追踪逻辑。也就是说,如果你在文档里用setMonth()之类的方法修改了 Date,Mongoose 不会感知到这次改动,doc.save()也就不会持久化该修改。如果必须用内置方法修改 Date 类型,请在保存前调用doc.markModified('pathToYourDate')告知 Mongoose:
const Assignment = mongoose.model('Assignment', { dueDate: Date }); const doc = await Assignment.findOne(); doc.dueDate.setMonth(3); await doc.save(); // 这不会保存你的修改! doc.markModified('dueDate'); await doc.save(); // 生效Buffer
声明 Buffer path,可以使用Buffer全局构造函数或字符串'Buffer':
const schema1 = new Schema({ binData: Buffer }); // binData 会被 cast 为 Buffer const schema2 = new Schema({ binData: 'Buffer' }); // 等价 const Data = mongoose.model('Data', schema2);Mongoose 可以成功地把下面的值 cast 成 buffer:
const file1 = new Data({ binData: 'test'}); // {"type":"Buffer","data":[116,101,115,116]} const file2 = new Data({ binData: 72987 }); // {"type":"Buffer","data":[27]} const file4 = new Data({ binData: { type: 'Buffer', data: [1, 2, 3]}}); // {"type":"Buffer","data":[1,2,3]}Mixed
Mixed 是"什么都可以"的 SchemaType。Mongoose 不会对 Mixed path 做任何 cast。可以用Schema.Types.Mixed或空对象字面量来定义 Mixed path,下面几种写法等价:
const Any = new Schema({ any: {} }); const Any = new Schema({ any: Object }); const Any = new Schema({ any: Schema.Types.Mixed }); const Any = new Schema({ any: mongoose.Mixed });由于 Mixed 是无 schema 的类型,你可以随意把值改成任何内容,但 Mongoose 会失去自动检测并保存这些修改的能力。要告诉 Mongoose Mixed 类型的值发生了变化,需要调用doc.markModified(path),并传入刚改过的 Mixed 类型的 path:
person.anything = { x: [3, 4, { y: 'changed' }] }; person.markModified('anything'); person.save(); // Mongoose 会保存对 `anything` 的修改。为避免这些副作用,也可以改用 Subdocument path。
ObjectIds
ObjectId 是通常用于唯一标识符的特殊类型。下面声明一个driver为 ObjectId 的 schema:
const mongoose = require('mongoose'); const carSchema = new mongoose.Schema({ driver: mongoose.ObjectId });ObjectId是一个类,ObjectId 是对象,但通常被表示为字符串。用toString()把 ObjectId 转成字符串时,会得到 24 位十六进制字符串:
const Car = mongoose.model('Car', carSchema); const car = new Car(); car.driver = new mongoose.Types.ObjectId(); typeof car.driver; // 'object' car.driver instanceof mongoose.Types.ObjectId; // true car.driver.toString(); // 类似 "5e1a0651741b255ddda996c4"Boolean
Mongoose 中的 Boolean 是原生 JavaScript 布尔值。默认情况下,Mongoose 把下面的值 cast 为true:
true'true'1'1''yes'
把下面的值 cast 为false:
false'false'0'0''no'
任何其他值都会导致 CastError。你可以通过convertToTrue和convertToFalse属性修改 Mongoose 转成 true/false 的值集合,这两个属性都是 JavaScript Set:
const M = mongoose.model('Test', new Schema({ b: Boolean })); console.log(new M({ b: 'nay' }).b); // undefined // Set { false, 'false', 0, '0', 'no' } console.log(mongoose.Schema.Types.Boolean.convertToFalse); mongoose.Schema.Types.Boolean.convertToFalse.add('nay'); console.log(new M({ b: 'nay' }).b); // false从源码看,默认转换集合定义在 lib/cast/boolean.js 中:convertToTrue = new Set([true, 'true', 1, '1', 'yes'])、convertToFalse = new Set([false, 'false', 0, '0', 'no']);lib/schema/boolean.js 通过Object.defineProperty把这两个集合暴露为SchemaBoolean的静态属性,并额外提供了SchemaBoolean.cast(caster)静态方法,可整体替换 cast 函数(传false则退回严格模式,只接受原生 boolean)。仓库的 test/schema.boolean.test.js 有大量针对这些 cast 行为与convertToTrue/convertToFalse扩展的测试用例。
Arrays
Mongoose 支持 SchemaType 数组和子文档(subdocument)数组。SchemaType 数组也叫primitive arrays(基础类型数组),子文档数组也叫document arrays(文档数组):
const ToySchema = new Schema({ name: String }); const ToyBoxSchema = new Schema({ toys: [ToySchema], buffers: [Buffer], strings: [String], numbers: [Number] // ... 等等 });数组是特殊的,因为它们隐式地有一个默认值[](空数组):
const ToyBox = mongoose.model('ToyBox', ToyBoxSchema); console.log((new ToyBox()).toys); // []要覆盖这个默认值,需要把默认值设为undefined:
const ToyBoxSchema = new Schema({ toys: { type: [ToySchema], default: undefined } });注意default应用在它声明的层级上。上面的例子中default紧挨着type: [ToySchema],所以它是数组的默认值。如果把它放进方括号内,它就成了数组中每个元素的默认值,而数组仍保留隐式的[]默认值:
const ArrayDefault = new Schema({ toys: { type: [String], default: undefined } }); const ElementDefault = new Schema({ // 这里的 `default: undefined` 作用于每个字符串元素,而不是 `toys` toys: [{ type: String, default: undefined }] }); mongoose.model('ArrayDefault', ArrayDefault); mongoose.model('ElementDefault', ElementDefault); new (mongoose.model('ArrayDefault'))().toys; // undefined new (mongoose.model('ElementDefault'))().toys; // []注意:指定空数组等价于Mixed。下面几种写法都创建Mixed数组:
const Empty1 = new Schema({ any: [] }); const Empty2 = new Schema({ any: Array }); const Empty3 = new Schema({ any: [Schema.Types.Mixed] }); const Empty4 = new Schema({ any: [{}] });Maps
MongooseMap是 JavaScriptMap类的子类。在 Mongoose 中,map 是创建带任意键的嵌套文档的方式。
注意:在 Mongoose Map 中,键必须是字符串,这样才能在 MongoDB 中存储文档。
const userSchema = new Schema({ // `socialMediaHandles` 是一个值类型为字符串的 map。 // map 的键始终是字符串,用 `of` 指定值的类型。 socialMediaHandles: { type: Map, of: String } }); const User = mongoose.model('User', userSchema); // Map { 'github' => 'vkarpov15', 'twitter' => '@code_barbarian' } console.log(new User({ socialMediaHandles: { github: 'vkarpov15', twitter: '@code_barbarian' } }).socialMediaHandles);上面的例子没有显式声明github或twitter为 path,但因为socialMediaHandles是 map,可以存储任意键值对。不过由于它是 map,你必须用.get()获取键值、用.set()设置键值:
const user = new User({ socialMediaHandles: {} }); // 正确 user.socialMediaHandles.set('github', 'vkarpov15'); // 也可以 user.set('socialMediaHandles.twitter', '@code_barbarian'); // 错误,`myspace` 属性不会被保存 user.socialMediaHandles.myspace = 'fail'; // 'vkarpov15' console.log(user.socialMediaHandles.get('github')); // '@code_barbarian' console.log(user.get('socialMediaHandles.twitter')); // undefined user.socialMediaHandles.github; // 只会保存 'github' 和 'twitter' 属性 user.save();Map 类型在 MongoDB 中以 BSON 对象存储。BSON 对象的键是有序的,因此 map 的插入顺序特性得以保留。
Mongoose 支持特殊的$*语法来 populate map 中的所有元素。例如假设socialMediaHandlesmap 中包含一个ref:
const userSchema = new Schema({ socialMediaHandles: { type: Map, of: new Schema({ handle: String, oauth: { type: ObjectId, ref: 'OAuth' } }) } }); const User = mongoose.model('User', userSchema);要 populate 每个socialMediaHandles条目的oauth属性,应该 populatesocialMediaHandles.$*.oauth:
const user = await User.findOne().populate('socialMediaHandles.$*.oauth');Map 的底层实现位于 lib/schema/map.js,它继承自 JavaScript 原生Map并挂接到 Mongoose 的变更追踪机制;仓库 test/types.map.test.js 覆盖了 map 的读写、cast 与$*populate 场景。
UUID
Mongoose 还支持 UUID 类型,它把 UUID 实例以 Node.js buffer 形式存储。在 Node.js 中,UUID 表示为bson.Binary类型的实例,并带有一个 getter,在访问时把二进制转成字符串。Mongoose 在 MongoDB 中以 subtype 4 的二进制数据存储 UUID。
建议:在 Mongoose 中做唯一文档 id 时优先使用 ObjectId,只有在确有需要时才使用 UUID。
const authorSchema = new Schema({ _id: Schema.Types.UUID, // 也可以写 `_id: 'UUID'` name: String }); const Author = mongoose.model('Author', authorSchema); const bookSchema = new Schema({ authorId: { type: Schema.Types.UUID, ref: 'Author' } }); const Book = mongoose.model('Book', bookSchema); const author = new Author({ name: 'Martin Fowler' }); console.log(typeof author._id); // 'string' console.log(author.toObject()._id instanceof mongoose.mongo.BSON.Binary); // true const book = new Book({ authorId: '09190f70-3d30-11e5-8814-0f4df9a59c41' });创建 UUID 时,推荐使用 Node 内置的 UUIDv4 生成器:
const { randomUUID } = require('crypto'); const schema = new mongoose.Schema({ docId: { type: 'UUID', default: () => randomUUID() } });BigInt
Mongoose 支持把 JavaScript BigInt 作为 SchemaType。BigInt 在 MongoDB 中以 64 位整数(BSON 类型 "long")存储:
const questionSchema = new Schema({ answer: BigInt }); const Question = mongoose.model('Question', questionSchema); const question = new Question({ answer: 42n }); typeof question.answer; // 'bigint'Double
Mongoose 支持把 64 位 IEEE 754-2008 浮点数作为 SchemaType。Double 在 MongoDB 中以 BSON 类型 "double" 存储:
const temperatureSchema = new Schema({ celsius: Double }); const Temperature = mongoose.model('Temperature', temperatureSchema); const temperature = new Temperature({ celsius: 1339 }); temperature.celsius instanceof bson.Double; // true下面这些值都能成功 cast 为 Double:
new Temperature({ celsius: '1.2e12' }).celsius; // 1200000000000 作为 Double new Temperature({ celsius: true }).celsius; // 1 作为 Double new Temperature({ celsius: false }).celsius; // 0 作为 Double new Temperature({ celsius: { valueOf: () => 83.0033 } }).celsius; // 83 作为 Double new Temperature({ celsius: '' }).celsius; // null以下输入会在验证阶段导致 CastError(初始化时不抛错,验证时才抛错):
- 不代表数字字符串、NaN 或 null-ish 值的字符串
- 没有
valueOf()函数的对象 - 超出 IEEE 754-2008 浮点数表示范围的值
从 lib/schema/double.js 源码可见,SchemaDouble的查询条件处理器($conditionalHandlers)为$gt/$gte/$lt/$lte等比较操作符单独注册了 cast 逻辑,并支持SchemaDouble.cast(caster)自定义 cast 函数(如把 NaN cast 成 0)。
Int32
Mongoose 支持把 32 位整数作为 SchemaType。Int32 在 MongoDB 中以 32 位整数(BSON 类型 "int")存储:
const studentSchema = new Schema({ id: Int32 }); const Student = mongoose.model('Student', studentSchema); const student = new Student({ id: 1339 }); typeof student.id; // 'number'下面这些值都能成功 cast 为 Int32:
new Student({ id: '15' }).id; // 15 作为 Int32 new Student({ id: true }).id; // 1 作为 Int32 new Student({ id: false }).id; // 0 作为 Int32 new Student({ id: { valueOf: () => 83 } }).id; // 83 作为 Int32 new Student({ id: '' }).id; // null 作为 Int32如果传入的对象带有返回 Number 的valueOf()函数,Mongoose 会调用它并把返回值赋给该 path。null和undefined不会被 cast。
以下输入会在验证阶段导致 CastError(初始化时不抛错,验证时才抛错):
- NaN
- 能 cast 成 NaN 的字符串
- 没有
valueOf()函数的对象 - 必须四舍五入才能成为整数的小数
- 超出 32 位整数范围的值
从 lib/schema/int32.js 源码可以看到,SchemaInt32的默认严格 caster 用INT32_MAX = 0x7FFFFFFF、INT32_MIN = -0x80000000做边界检查(v !== (v | 0)还会校验是否为整数),并且它的$conditionalHandlers额外注册了$bitsAllClear、$bitsAnyClear、$bitsAllSet、$bitsAnySet四个位运算操作符(见 lib/schema/operators/bitwise.js)。
Union
UnionSchemaType 允许一个 path 接受多种类型。Mongoose 会尝试把值 cast 为其中一种指定类型:
const schema = new Schema({ value: { type: Schema.Types.Union, of: [String, Number] } }); const Model = mongoose.model('Model', schema); // 两种都有效 —— Mongoose 接受任意一种类型 const doc1 = new Model({ value: 'hello' }); const doc2 = new Model({ value: 42 });Casting 行为
当你给 Union path 赋值时,Mongoose 按顺序尝试把它 cast 为of数组中的每种类型。如果值与其中某个类型精确匹配(使用===),Mongoose 直接使用该值;否则使用第一个能成功 cast 该值的类型:
const schema = new Schema({ flexibleField: { type: Schema.Types.Union, of: [Number, Date] } }); const Model = mongoose.model('Model', schema); // Number 类型 const doc1 = new Model({ flexibleField: 42 }); doc1.flexibleField; // 42 (number) // 字符串 '42' 被 cast 为 Number(第一个成功的类型) const doc2 = new Model({ flexibleField: '42' }); doc2.flexibleField; // 42 (number) // Date 类型 const doc3 = new Model({ flexibleField: new Date('2025-06-01') }); doc3.flexibleField; // Date 对象 // 字符串日期被 cast 为 Date const doc4 = new Model({ flexibleField: '2025-06-01' }); doc4.flexibleField; // Date 对象Union 的 cast 逻辑实现于 lib/schema/union.js:构造函数会校验options.of必须是非空数组(否则直接抛出'Union schema type requires an array of types'),并把每个成员通过parentSchema.interpretAsType()解释成真实的 SchemaType 实例存进this.schemaTypes;cast()方法依次调用每个子类型的 cast,命中=== val立即返回原值(避免 Number 被错误 cast 成 String/Date 等),否则取第一个成功 cast 的结果,全部失败则抛出最后一个类型的 cast 错误。
错误处理
如果 Mongoose 无法把值 cast 为任何指定类型,它会抛出 union 中最后一个类型产生的错误:
const schema = new Schema({ value: { type: Schema.Types.Union, of: [Number, Boolean] } }); const Model = mongoose.model('Model', schema); const doc = new Model({ value: 'not a number or boolean' }); // 抛出: Cast to Boolean failed for value "not a number or boolean"Union 带选项
你可以为 union 中的单个类型指定选项,例如为字符串指定trim:
const schema = new Schema({ value: { type: Schema.Types.Union, of: [ Number, { type: String, trim: true } ] } }); const Model = mongoose.model('Model', schema); const doc = new Model({ value: ' hello ' }); doc.value; // 'hello' (已 trim)查询与更新
Union 类型同样适用于查询和更新。Mongoose 会根据 union 类型来 cast 查询过滤条件和更新操作:
const schema = new Schema({ value: { type: Schema.Types.Union, of: [Number, Date] } }); const Model = mongoose.model('Model', schema); await Model.create({ value: 42 }); // 用字符串查询 —— 会被 cast 为数字 const doc = await Model.findOne({ value: '42' }); doc.value; // 42 // 更新 await Model.findOneAndUpdate( { value: 42 }, { value: new Date('2025-06-01') } );此外,Union.prototype.toJSONSchema()(见 lib/schema/union.js)会把 union 输出为 JSON Schema 的anyOf结构,可用于 MongoDB 的$jsonSchema校验或加密 schema 配置;applySetters()也会为 union 中的每个成员依次应用其 setter 后再 cast。仓库 test/schema.union.test.js 提供了覆盖上述 cast、错误处理、选项与查询更新场景的测试。
Getters
Getter 之于 path 就像 virtuals 之于整个文档。例如你想把用户头像存为相对路径,再在应用层拼接主机名,可以这样组织userSchema:
const root = 'https://s3.amazonaws.com/mybucket'; const userSchema = new Schema({ name: String, picture: { type: String, get: v => `${root}${v}` } }); const User = mongoose.model('User', userSchema); const doc = new User({ name: 'Val', picture: '/123.png' }); doc.picture; // 'https://s3.amazonaws.com/mybucket/123.png' doc.toObject({ getters: false }).picture; // '/123.png'通常只在基础类型 path上使用 getter,而不是数组或子文档。因为 getter 会覆盖访问 Mongoose path 时返回的内容,在对象上声明 getter 可能会移除 Mongoose 对该 path 的变更追踪:
const schema = new Schema({ arr: [{ url: String }] }); const root = 'https://s3.amazonaws.com/mybucket'; // 不好,不要这样做! schema.path('arr').get(v => { return v.map(el => Object.assign(el, { url: root + el.url })); }); // 之后 doc.arr.push({ key: String }); doc.arr[0]; // 'undefined',因为每次访问 `doc.arr` 都会创建新数组!不要像上面那样在数组上声明 getter,而应该在url字符串上声明 getter。如果确实需要在嵌套文档或数组上声明 getter,请格外小心:
const schema = new Schema({ arr: [{ url: String }] }); const root = 'https://s3.amazonaws.com/mybucket'; // 正确:替代在 `arr` 上声明 getter 的做法 schema.path('arr.0.url').get(v => `${root}${v}`);用 Schema 作为路径类型
要把某个 path 声明为另一个 schema,将type设置为子 schema 的实例即可。要基于子 schema 的形状设置默认值,直接设置一个默认值,文档创建期间该值会先按子 schema 定义被 cast 再设置:
const subSchema = new mongoose.Schema({ // 这里放一些 schema 定义 }); const schema = new mongoose.Schema({ data: { type: subSchema, default: {} } });创建自定义 SchemaTypes
Mongoose 可以通过自定义 SchemaTypes 进行扩展,完整指南见 Custom SchemaTypes 文档。你也可以在插件站点搜索兼容的类型,例如 mongoose-long、mongoose-int32、mongoose-function 等。所有自定义类型最终都要像内置类型一样提供schemaName、cast 逻辑与OptionsConstructor(可参考 lib/schema/index.js 中内置类型的注册方式)。
schema.path()函数
schema.path()返回指定 path 实例化后的 schema type:
const sampleSchema = new Schema({ name: { type: String, required: true } }); console.log(sampleSchema.path('name')); // 输出类似: /** * SchemaString { * enumValues: [], * regExp: null, * path: 'name', * instance: 'String', * validators: ... * } */可以用这个函数检查某个 path 的 schema type,包括它有哪些校验器以及类型是什么。例如前文验证schema.path('name') instanceof mongoose.Schema.Types.String就是通过它实现的。
进一步阅读与下一步
围绕 SchemaTypes 体系,建议按以下顺序继续深入本仓库:
- 校验(Validation):
required、enum、min/max、match等内置校验器的完整说明与 CastError 行为 - Schema 指南:schema 定义、
type关键字与 virtuals 的更多细节 - Populate:
populate选项与 Map 的$*populate 语法 - Subdocs(子文档):document array 与子文档变更追踪
- Dates 教程:Date 类型的更多使用细节
- 自定义 SchemaTypes 指南:编写插件级自定义类型
- Connections:在掌握 SchemaTypes 后学习连接管理,也是官方文档的"下一步"章节
掌握 SchemaTypes 之后,就可以继续学习 Mongoose 的 Connections,搭建完整的异步数据访问链路了。
【免费下载链接】mongooseMongoDB object modeling designed to work in an asynchronous environment.项目地址: https://gitcode.com/GitHub_Trending/mo/mongoose
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考