模式
如果您还没有这样做,请花点时间阅读快速入门,了解 Mongoose 的工作原理。如果您要从 7.x 迁移到 8.x,请花点时间阅读迁移指南。
定义您的模式
Mongoose 中的一切都从模式开始。每个模式映射到一个 MongoDB 集合,并定义该集合中文档的形状。
import mongoose from 'mongoose';
const { Schema } = mongoose;
const blogSchema = new Schema({
title: String, // String is shorthand for {type: String}
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
如果您想稍后添加额外的键,请使用Schema#add方法。
我们代码中 blogSchema
中的每个键都定义了文档中的一个属性,该属性将被转换为其关联的SchemaType。例如,我们定义了一个属性 title
,它将被转换为String SchemaType,以及属性 date
,它将被转换为 Date
SchemaType。
请注意,如果属性只需要一个类型,则可以使用简写符号来指定(将上面的 title
属性与上面的 date
属性进行对比)。
键也可以被分配包含更多键/类型定义的嵌套对象,就像上面的 meta
属性一样。只要键的值是一个没有 type
属性的 POJO,就会发生这种情况。
在这些情况下,Mongoose 只为树中的叶子创建实际的模式路径(如上面的 meta.votes
和 meta.favs
),而分支没有实际路径。这种行为的副作用是,上面的 meta
无法进行自身的验证。如果需要对树进行验证,则需要在树上创建路径 - 有关如何执行此操作的更多信息,请参阅子文档部分。此外,请阅读混合SchemaTypes 指南中的部分,了解一些注意事项。
允许的 SchemaTypes 为
有关SchemaTypes的更多信息,请点击此处。
模式不仅定义了文档的结构和属性的转换,还定义了文档的实例方法、静态模型方法、复合索引以及称为中间件的文档生命周期钩子。
创建模型
要使用我们的模式定义,我们需要将 blogSchema
转换为我们可以使用的模型。为此,我们将它传递给 mongoose.model(modelName, schema)
const Blog = mongoose.model('Blog', blogSchema);
// ready to go!
ID
默认情况下,Mongoose 会在您的模式中添加一个 _id
属性。
const schema = new Schema();
schema.path('_id'); // ObjectId { ... }
当您使用自动添加的 _id
属性创建一个新文档时,Mongoose 会在您的文档中创建一个新的_id
类型为 ObjectId。
const Model = mongoose.model('Test', schema);
const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true
您也可以用自己的 _id
覆盖 Mongoose 的默认 _id
。请注意:Mongoose 会拒绝保存没有 _id
的顶级文档,因此如果您定义了自己的 _id
路径,则需要负责设置 _id
。
const schema = new Schema({
_id: Number // <-- overwrite Mongoose's default `_id`
});
const Model = mongoose.model('Test', schema);
const doc = new Model();
await doc.save(); // Throws "document must have an _id before saving"
doc._id = 1;
await doc.save(); // works
Mongoose 也会在子文档中添加一个 _id
属性。您可以通过以下方式禁用子文档上的 _id
属性。Mongoose 允许保存没有 _id
属性的子文档。
const nestedSchema = new Schema(
{ name: String },
{ _id: false } // <-- disable `_id`
);
const schema = new Schema({
subdoc: nestedSchema,
docArray: [nestedSchema]
});
const Test = mongoose.model('Test', schema);
// Neither `subdoc` nor `docArray.0` will have an `_id`
await Test.create({
subdoc: { name: 'test 1' },
docArray: [{ name: 'test 2' }]
});
或者,您可以使用以下语法禁用 _id
const nestedSchema = new Schema({
_id: false, // <-- disable _id
name: String
});
实例方法
Models
的实例是文档。文档具有许多它们自己的内置实例方法。我们也可以定义我们自己的自定义文档实例方法。
// define a schema
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "methods" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the instance functions.
methods: {
findSimilarTypes(cb) {
return mongoose.model('Animal').find({ type: this.type }, cb);
}
}
});
// Or, assign a function to the "methods" object of our animalSchema
animalSchema.methods.findSimilarTypes = function(cb) {
return mongoose.model('Animal').find({ type: this.type }, cb);
};
现在,我们所有的 animal
实例都拥有一个可用的 findSimilarTypes
方法。
const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({ type: 'dog' });
dog.findSimilarTypes((err, dogs) => {
console.log(dogs); // woof
});
- 覆盖默认的 mongoose 文档方法可能会导致不可预知的结果。有关更多详细信息,请参阅此内容。
- 上面的示例直接使用
Schema.methods
对象来保存实例方法。您也可以使用此处所述的Schema.method()
助手。 - 不要使用 ES6 箭头函数(
=>
)声明方法。箭头函数明确地阻止绑定this
,因此您的方法将无法访问文档,上面的示例将无法正常工作。
静态方法
您还可以向模型添加静态函数。有三种等效的方式来添加静态方法
- 在模式构造函数(
statics
)的第二个参数中添加一个函数属性 - 在
schema.statics
中添加一个函数属性 - 调用
Schema#static()
函数
// define a schema
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "statics" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the statics functions.
statics: {
findByName(name) {
return this.find({ name: new RegExp(name, 'i') });
}
}
});
// Or, Assign a function to the "statics" object of our animalSchema
animalSchema.statics.findByName = function(name) {
return this.find({ name: new RegExp(name, 'i') });
};
// Or, equivalently, you can call `animalSchema.static()`.
animalSchema.static('findByBreed', function(breed) { return this.find({ breed }); });
const Animal = mongoose.model('Animal', animalSchema);
let animals = await Animal.findByName('fido');
animals = animals.concat(await Animal.findByBreed('Poodle'));
不要使用 ES6 箭头函数(=>
)声明静态方法。箭头函数明确地阻止绑定 this
,因此上面的示例将无法正常工作,因为 this
的值。
查询助手
您还可以添加查询助手函数,它们类似于实例方法,但适用于 mongoose 查询。查询助手方法让您可以扩展 mongoose 的可链式查询构建器 API。
// define a schema
const animalSchema = new Schema({ name: String, type: String },
{
// Assign a function to the "query" object of our animalSchema through schema options.
// By following this approach, there is no need to create a separate TS type to define the type of the query functions.
query: {
byName(name) {
return this.where({ name: new RegExp(name, 'i') });
}
}
});
// Or, Assign a function to the "query" object of our animalSchema
animalSchema.query.byName = function(name) {
return this.where({ name: new RegExp(name, 'i') });
};
const Animal = mongoose.model('Animal', animalSchema);
Animal.find().byName('fido').exec((err, animals) => {
console.log(animals);
});
Animal.findOne().byName('fido').exec((err, animal) => {
console.log(animal);
});
索引
MongoDB 支持辅助索引。使用 mongoose,我们可以在 Schema
中 的 路径 级别或 schema
级别定义这些索引。在创建复合索引时,必须在模式级别定义索引。
const animalSchema = new Schema({
name: String,
type: String,
tags: { type: [String], index: true } // path level
});
animalSchema.index({ name: 1, type: -1 }); // schema level
有关其他索引选项,请参阅SchemaType#index()。
当您的应用程序启动时,Mongoose 会自动调用createIndex
,为您的模式中定义的每个索引创建索引。Mongoose 会按顺序为每个索引调用 createIndex
,并在所有 createIndex
调用成功或出现错误时,在模型上发出 'index' 事件。虽然这在开发中很好用,但建议在生产环境中禁用此行为,因为索引创建会造成显著的性能影响。通过将模式的 autoIndex
选项设置为 false
来禁用此行为,或者通过在连接上全局设置 autoIndex
选项为 false
来禁用此行为。
mongoose.connect('mongodb://user:[email protected]:port/database', { autoIndex: false });
// or
mongoose.createConnection('mongodb://user:[email protected]:port/database', { autoIndex: false });
// or
mongoose.set('autoIndex', false);
// or
animalSchema.set('autoIndex', false);
// or
new Schema({ /* ... */ }, { autoIndex: false });
当索引构建完成或出现错误时,Mongoose 会在模型上发出 'index' 事件。
// Will cause an error because mongodb has an _id index by default that
// is not sparse
animalSchema.index({ _id: 1 }, { sparse: true });
const Animal = mongoose.model('Animal', animalSchema);
Animal.on('index', error => {
// "_id index cannot be sparse"
console.log(error.message);
});
另请参阅Model#ensureIndexes方法。
虚拟属性
虚拟属性是文档属性,您可以获取和设置它们,但它们不会持久保存到 MongoDB。getter 用于格式化或组合字段,而 setter 用于将单个值分解为多个值以供存储。
// define a schema
const personSchema = new Schema({
name: {
first: String,
last: String
}
});
// compile our model
const Person = mongoose.model('Person', personSchema);
// create a document
const axl = new Person({
name: { first: 'Axl', last: 'Rose' }
});
假设您想打印出该人的全名。您可以自己动手
console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose
但是,每次都连接姓氏和名字可能会很麻烦。如果您想对姓名进行一些额外的处理,例如删除变音符呢?虚拟属性 getter 允许您定义一个 fullName
属性,该属性不会持久保存到 MongoDB。
// That can be done either by adding it to schema options:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
virtuals: {
fullName: {
get() {
return this.name.first + ' ' + this.name.last;
}
}
}
});
// Or by using the virtual method as following:
personSchema.virtual('fullName').get(function() {
return this.name.first + ' ' + this.name.last;
});
现在,每次您访问 fullName
属性时,mongoose 都会调用您的 getter 函数
console.log(axl.fullName); // Axl Rose
如果您使用 toJSON()
或 toObject()
,Mongoose 默认情况下不会包含虚拟属性。将 { virtuals: true }
传递给toJSON()
或 toObject()
以包含虚拟属性。
// Convert `doc` to a POJO, with virtuals attached
doc.toObject({ virtuals: true });
// Equivalent:
doc.toJSON({ virtuals: true });
上面关于 toJSON()
的警告也包括对 Mongoose 文档调用JSON.stringify()
的输出,因为JSON.stringify()
会调用 toJSON()
。要在 JSON.stringify()
输出中包含虚拟属性,您可以在调用 JSON.stringify()
之前对文档调用 toObject({ virtuals: true })
,或者在您的模式上设置 toJSON: { virtuals: true }
选项。
// Explicitly add virtuals to `JSON.stringify()` output
JSON.stringify(doc.toObject({ virtuals: true }));
// Or, to automatically attach virtuals to `JSON.stringify()` output:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
toJSON: { virtuals: true } // <-- include virtuals in `JSON.stringify()`
});
您还可以向虚拟属性添加自定义 setter,这将允许您通过 fullName
虚拟属性同时设置姓氏和名字。
// Again that can be done either by adding it to schema options:
const personSchema = new Schema({
name: {
first: String,
last: String
}
}, {
virtuals: {
fullName: {
get() {
return this.name.first + ' ' + this.name.last;
},
set(v) {
this.name.first = v.substr(0, v.indexOf(' '));
this.name.last = v.substr(v.indexOf(' ') + 1);
}
}
}
});
// Or by using the virtual method as following:
personSchema.virtual('fullName').
get(function() {
return this.name.first + ' ' + this.name.last;
}).
set(function(v) {
this.name.first = v.substr(0, v.indexOf(' '));
this.name.last = v.substr(v.indexOf(' ') + 1);
});
axl.fullName = 'William Rose'; // Now `axl.name.first` is "William"
虚拟属性 setter 在其他验证之前应用。因此,即使 first
和 last
名称字段是必需的,上面的示例仍然可以正常工作。
只有非虚拟属性才能用作查询的一部分,以及用于字段选择。由于虚拟属性不存储在 MongoDB 中,因此您无法用它们进行查询。
您可以在此处了解有关虚拟属性的更多信息。
别名
别名是一种特殊的虚拟属性,getter 和 setter 可无缝地获取和设置另一个属性。这对节省网络带宽很有用,因此您可以将数据库中存储的简短属性名称转换为更长的名称,以提高代码可读性。
const personSchema = new Schema({
n: {
type: String,
// Now accessing `name` will get you the value of `n`, and setting `name` will set the value of `n`
alias: 'name'
}
});
// Setting `name` will propagate to `n`
const person = new Person({ name: 'Val' });
console.log(person); // { n: 'Val' }
console.log(person.toObject({ virtuals: true })); // { n: 'Val', name: 'Val' }
console.log(person.name); // "Val"
person.name = 'Not Val';
console.log(person); // { n: 'Not Val' }
您也可以在嵌套路径上声明别名。使用嵌套模式和子文档更方便,但您也可以在内联声明嵌套路径别名,只要您使用完整的嵌套路径 nested.myProp
作为别名即可。
const childSchema = new Schema({
n: {
type: String,
alias: 'name'
}
}, { _id: false });
const parentSchema = new Schema({
// If in a child schema, alias doesn't need to include the full nested path
c: childSchema,
name: {
f: {
type: String,
// Alias needs to include the full nested path if declared inline
alias: 'name.first'
}
}
});
选项
模式有一些可配置的选项,可以将这些选项传递给构造函数或 set
方法
new Schema({ /* ... */ }, options);
// or
const schema = new Schema({ /* ... */ });
schema.set(option, value);
有效选项
- autoIndex
- autoCreate
- bufferCommands
- bufferTimeoutMS
- capped
- collection
- discriminatorKey
- excludeIndexes
- id
- _id
- minimize
- read
- writeConcern
- shardKey
- statics
- strict
- strictQuery
- toJSON
- toObject
- typeKey
- validateBeforeSave
- versionKey
- optimisticConcurrency
- collation
- timeseries
- selectPopulatedPaths
- skipVersioning
- timestamps
- storeSubdocValidationError
- collectionOptions
- methods
- query
- autoSearchIndex
- readConcern
选项:autoIndex
默认情况下,Mongoose 的init()
函数 通过在您成功连接到 MongoDB 后调用Model.createIndexes()
来创建模型模式中定义的所有索引。自动创建索引非常适合开发和测试环境。但是,索引构建也会给您的生产数据库带来很大的负载。如果您想在生产环境中仔细管理索引,则可以将 autoIndex
设置为 false。
const schema = new Schema({ /* ... */ }, { autoIndex: false });
const Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);
autoIndex
选项默认设置为 true
。您可以通过设置mongoose.set('autoIndex', false);
来更改此默认值。
选项:autoCreate
在 Mongoose 构建索引之前,默认情况下,它会调用 Model.createCollection()
在 MongoDB 中创建底层集合。调用 createCollection()
会根据 集合的默认排序规则 设置集合的默认排序规则,并根据 排序规则选项 设置集合。如果您设置了 capped
架构选项,则将集合建立为定长集合。
您可以通过使用 mongoose.set('autoCreate', false)
将 autoCreate
设置为 false
来禁用此行为。与 autoIndex
一样,autoCreate
对开发和测试环境很有帮助,但您可能希望在生产环境中禁用它,以避免不必要的数据库调用。
不幸的是,createCollection()
无法更改现有集合。例如,如果您将 capped: { size: 1024 }
添加到您的架构,而现有集合不是定长集合,则 createCollection()
不会覆盖现有集合。这是因为 MongoDB 服务器不允许在不首先删除集合的情况下更改集合的选项。
const schema = new Schema({ name: String }, {
autoCreate: false,
capped: { size: 1024 }
});
const Test = mongoose.model('Test', schema);
// No-op if collection already exists, even if the collection is not capped.
// This means that `capped` won't be applied if the 'tests' collection already exists.
await Test.createCollection();
选项:bufferCommands
默认情况下,当连接断开时,mongoose 会缓冲命令,直到驱动程序设法重新连接。要禁用缓冲,请将 bufferCommands
设置为 false。
const schema = new Schema({ /* ... */ }, { bufferCommands: false });
架构 bufferCommands
选项会覆盖全局 bufferCommands
选项。
mongoose.set('bufferCommands', true);
// Schema option below overrides the above, if the schema option is set.
const schema = new Schema({ /* ... */ }, { bufferCommands: false });
选项:bufferTimeoutMS
如果 bufferCommands
已开启,则此选项设置 Mongoose 缓冲在抛出错误之前等待的最大时间。如果未指定,Mongoose 将使用 10000(10 秒)。
// If an operation is buffered for more than 1 second, throw an error.
const schema = new Schema({ /* ... */ }, { bufferTimeoutMS: 1000 });
选项:capped
Mongoose 支持 MongoDB 的 定长集合。要指定底层 MongoDB 集合为 capped
,请将 capped
选项设置为集合的最大大小(以 字节 为单位)。
new Schema({ /* ... */ }, { capped: 1024 });
如果您想传递其他选项(如 max),则 capped
选项也可以设置为对象。在这种情况下,您必须显式传递 size
选项,这是必需的。
new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } });
选项:collection
默认情况下,Mongoose 会通过将模型名称传递给 utils.toCollectionName
方法来生成集合名称。此方法会将名称复数化。如果您需要为您的集合使用不同的名称,请设置此选项。
const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });
选项:discriminatorKey
当您定义一个 鉴别器 时,Mongoose 会在您的架构中添加一个路径,用于存储文档是哪个鉴别器的实例。默认情况下,Mongoose 会添加一个 __t
路径,但您可以设置 discriminatorKey
来覆盖此默认值。
const baseSchema = new Schema({}, { discriminatorKey: 'type' });
const BaseModel = mongoose.model('Test', baseSchema);
const personSchema = new Schema({ name: String });
const PersonModel = BaseModel.discriminator('Person', personSchema);
const doc = new PersonModel({ name: 'James T. Kirk' });
// Without `discriminatorKey`, Mongoose would store the discriminator
// key in `__t` instead of `type`
doc.type; // 'Person'
选项:excludeIndexes
当 excludeIndexes
为 true
时,Mongoose 不会从给定的子文档架构中创建索引。此选项仅在架构用于子文档路径或文档数组路径时才有效,如果在模型的顶层架构上设置,Mongoose 会忽略此选项。默认为 false
。
const childSchema1 = Schema({
name: { type: String, index: true }
});
const childSchema2 = Schema({
name: { type: String, index: true }
}, { excludeIndexes: true });
// Mongoose will create an index on `child1.name`, but **not** `child2.name`, because `excludeIndexes`
// is true on `childSchema2`
const User = new Schema({
name: { type: String, index: true },
child1: childSchema1,
child2: childSchema2
});
选项:id
默认情况下,Mongoose 会为您的每个架构分配一个 id
虚拟 getter,它返回文档的 _id
字段,将其转换为字符串,或者在 ObjectId 的情况下,将其转换为十六进制字符串。如果您不希望将 id
getter 添加到您的架构中,可以在架构构造时通过传递此选项来禁用它。
// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // '50341373e894ad16347efe01'
// disabled id
const schema = new Schema({ name: String }, { id: false });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p.id); // undefined
选项:_id
默认情况下,如果未将 _id
传递到 Schema 构造函数,Mongoose 会为您的每个架构分配一个 _id
字段。分配的类型是 ObjectId,以与 MongoDB 的默认行为一致。如果您根本不希望将 _id
添加到您的架构中,可以使用此选项来禁用它。
您只能在子文档上使用此选项。Mongoose 无法在不知道文档 ID 的情况下保存文档,因此如果您尝试在没有 _id
的情况下保存文档,您将收到错误。
// default behavior
const schema = new Schema({ name: String });
const Page = mongoose.model('Page', schema);
const p = new Page({ name: 'mongodb.org' });
console.log(p); // { _id: '50341373e894ad16347efe01', name: 'mongodb.org' }
// disabled _id
const childSchema = new Schema({ name: String }, { _id: false });
const parentSchema = new Schema({ children: [childSchema] });
const Model = mongoose.model('Model', parentSchema);
Model.create({ children: [{ name: 'Luke' }] }, (error, doc) => {
// doc.children[0]._id will be undefined
});
选项:minimize
默认情况下,Mongoose 会通过删除空对象来“最小化”架构。
const schema = new Schema({ name: String, inventory: {} });
const Character = mongoose.model('Character', schema);
// will store `inventory` field if it is not empty
const frodo = new Character({ name: 'Frodo', inventory: { ringOfPower: 1 } });
await frodo.save();
let doc = await Character.findOne({ name: 'Frodo' }).lean();
doc.inventory; // { ringOfPower: 1 }
// will not store `inventory` field if it is empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // undefined
可以通过将 minimize
选项设置为 false
来覆盖此行为。然后它将存储空对象。
const schema = new Schema({ name: String, inventory: {} }, { minimize: false });
const Character = mongoose.model('Character', schema);
// will store `inventory` if empty
const sam = new Character({ name: 'Sam', inventory: {} });
await sam.save();
doc = await Character.findOne({ name: 'Sam' }).lean();
doc.inventory; // {}
要检查对象是否为空,可以使用 $isEmpty()
助手
const sam = new Character({ name: 'Sam', inventory: {} });
sam.$isEmpty('inventory'); // true
sam.inventory.barrowBlade = 1;
sam.$isEmpty('inventory'); // false
选项:read
允许在架构级别设置 query#read 选项,为我们提供了一种将默认 ReadPreferences 应用于从模型派生的所有查询的方法。
const schema = new Schema({ /* ... */ }, { read: 'primary' }); // also aliased as 'p'
const schema = new Schema({ /* ... */ }, { read: 'primaryPreferred' }); // aliased as 'pp'
const schema = new Schema({ /* ... */ }, { read: 'secondary' }); // aliased as 's'
const schema = new Schema({ /* ... */ }, { read: 'secondaryPreferred' }); // aliased as 'sp'
const schema = new Schema({ /* ... */ }, { read: 'nearest' }); // aliased as 'n'
每个 pref 的别名也被允许,因此我们可以简单地传递 'sp',而不是必须键入 'secondaryPreferred' 并拼写错误。
read 选项还允许我们指定标签集。这些告诉 驱动程序 从副本集的哪些成员尝试读取。详细了解标签集这里 和 这里。
注意:您也可以在连接时指定驱动程序读取首选项 strategy 选项
// pings the replset members periodically to track network latency
const options = { replset: { strategy: 'ping' } };
mongoose.connect(uri, options);
const schema = new Schema({ /* ... */ }, { read: ['nearest', { disk: 'ssd' }] });
mongoose.model('JellyBean', schema);
选项:writeConcern
允许在架构级别设置 写入关注。
const schema = new Schema({ name: String }, {
writeConcern: {
w: 'majority',
j: true,
wtimeout: 1000
}
});
选项:shardKey
shardKey
选项在使用 分片 MongoDB 架构 时使用。每个分片集合都会获得一个分片键,该分片键必须存在于所有插入/更新操作中。我们只需要将此架构选项设置为相同的分片键,我们就可以完成设置。
new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } });
请注意,Mongoose 不会为您发送 shardcollection
命令。您必须自己配置分片。
选项:strict
strict 选项(默认情况下启用)确保传递给模型构造函数但未在我们的架构中指定的值不会被保存到数据库。
const thingSchema = new Schema({ /* ... */ })
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is not saved to the db
// set to false..
const thingSchema = new Schema({ /* ... */ }, { strict: false });
const thing = new Thing({ iAmNotInTheSchema: true });
thing.save(); // iAmNotInTheSchema is now saved to the db!!
这也影响了 doc.set()
用于设置属性值的使用。
const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.set('iAmNotInTheSchema', true);
thing.save(); // iAmNotInTheSchema is not saved to the db
此值可以通过传递第二个布尔值参数在模型实例级别覆盖
const Thing = mongoose.model('Thing');
const thing = new Thing(doc, true); // enables strict mode
const thing = new Thing(doc, false); // disables strict mode
strict
选项也可以设置为 "throw"
,这会导致产生错误,而不是丢弃不良数据。
注意:在您的架构中不存在的实例上设置的任何键/值都会被忽略,无论架构选项如何。
const thingSchema = new Schema({ /* ... */ });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing;
thing.iAmNotInTheSchema = true;
thing.save(); // iAmNotInTheSchema is never saved to the db
选项:strictQuery
Mongoose 支持一个单独的 strictQuery
选项,以避免对查询过滤器使用严格模式。这是因为空查询过滤器会导致 Mongoose 返回模型中的所有文档,这可能会导致问题。
const mySchema = new Schema({ field: Number }, { strict: true });
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will filter out `notInSchema: 1` because `strict: true`, meaning this query will return
// _all_ documents in the 'tests' collection
MyModel.find({ notInSchema: 1 });
strict
选项确实适用于更新。strictQuery
选项仅适用于查询过滤器。
// Mongoose will strip out `notInSchema` from the update if `strict` is
// not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });
Mongoose 有一个单独的 strictQuery
选项,用于切换查询的 filter
参数的严格模式。
const mySchema = new Schema({ field: Number }, {
strict: true,
strictQuery: false // Turn off strict mode for query filters
});
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will not strip out `notInSchema: 1` because `strictQuery` is false
MyModel.find({ notInSchema: 1 });
通常,我们不建议将用户定义的对象作为查询过滤器传递
// Don't do this!
const docs = await MyModel.find(req.query);
// Do this instead:
const docs = await MyModel.find({ name: req.query.name, age: req.query.age }).setOptions({ sanitizeFilter: true });
在 Mongoose 7 中,strictQuery
默认情况下为 false
。但是,您可以全局覆盖此行为
// Set `strictQuery` to `true` to omit unknown fields in queries.
mongoose.set('strictQuery', true);
选项:toJSON
与 toObject 选项完全相同,但仅在调用文档的 toJSON
方法 时适用。
const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
return v + ' is my name';
});
schema.set('toJSON', { getters: true, virtuals: false });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m.toObject()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom' }
console.log(m.toJSON()); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
// since we know toJSON is called whenever a js object is stringified:
console.log(JSON.stringify(m)); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }
要查看所有可用的 toJSON/toObject
选项,请阅读 此内容。
选项:toObject
文档有一个 toObject 方法,该方法将 mongoose 文档转换为普通 JavaScript 对象。此方法接受一些选项。我们可以将这些选项在架构级别声明,而不是在每个文档的基础上应用这些选项,并将这些选项默认应用于该架构的所有文档。
要让所有虚拟值都显示在您的 console.log
输出中,请将 toObject
选项设置为 { getters: true }
const schema = new Schema({ name: String });
schema.path('name').get(function(v) {
return v + ' is my name';
});
schema.set('toObject', { getters: true });
const M = mongoose.model('Person', schema);
const m = new M({ name: 'Max Headroom' });
console.log(m); // { _id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
要查看所有可用的 toObject
选项,请阅读 此内容。
选项:typeKey
默认情况下,如果您的架构中有一个键为 'type' 的对象,mongoose 将将其解释为类型声明。
// Mongoose interprets this as 'loc is a String'
const schema = new Schema({ loc: { type: String, coordinates: [Number] } });
但是,对于 geoJSON 等应用程序,'type' 属性很重要。如果您想控制 mongoose 使用哪个键来查找类型声明,请设置 'typeKey' 架构选项。
const schema = new Schema({
// Mongoose interprets this as 'loc is an object with 2 keys, type and coordinates'
loc: { type: String, coordinates: [Number] },
// Mongoose interprets this as 'name is a String'
name: { $type: String }
}, { typeKey: '$type' }); // A '$type' key means this object is a type declaration
选项:validateBeforeSave
默认情况下,文档在保存到数据库之前会自动验证。这是为了防止保存无效的文档。如果您想手动处理验证,并能够保存未通过验证的对象,可以将 validateBeforeSave
设置为 false。
const schema = new Schema({ name: String });
schema.set('validateBeforeSave', false);
schema.path('name').validate(function(value) {
return value != null;
});
const M = mongoose.model('Person', schema);
const m = new M({ name: null });
m.validate(function(err) {
console.log(err); // Will tell you that null is not allowed.
});
m.save(); // Succeeds despite being invalid
选项:versionKey
versionKey
是在 Mongoose 首次创建每个文档时设置的属性。此键的值包含文档的内部 修订版。versionKey
选项是一个字符串,代表用于版本控制的路径。默认值为 __v
。如果这与您的应用程序冲突,您可以配置如下
const schema = new Schema({ name: 'string' });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
await thing.save(); // { __v: 0, name: 'mongoose v3' }
// customized versionKey
new Schema({ /* ... */ }, { versionKey: '_somethingElse' })
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'mongoose v3' });
thing.save(); // { _somethingElse: 0, name: 'mongoose v3' }
请注意,Mongoose 的默认版本控制不是完整的 乐观并发 解决方案。Mongoose 的默认版本控制仅对数组起作用,如下所示。
// 2 copies of the same document
const doc1 = await Model.findOne({ _id });
const doc2 = await Model.findOne({ _id });
// Delete first 3 comments from `doc1`
doc1.comments.splice(0, 3);
await doc1.save();
// The below `save()` will throw a VersionError, because you're trying to
// modify the comment at index 1, and the above `splice()` removed that
// comment.
doc2.set('comments.1.body', 'new comment');
await doc2.save();
如果您需要对 save()
提供乐观并发支持,可以设置 optimisticConcurrency
选项。
可以通过将 versionKey
设置为 false
来禁用文档版本控制。除非您知道自己在做什么,否则不要禁用版本控制。
new Schema({ /* ... */ }, { versionKey: false });
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({ name: 'no versioning please' });
thing.save(); // { name: 'no versioning please' }
Mongoose 仅在您使用 save()
时更新版本键。如果您使用 update()
、findOneAndUpdate()
等,Mongoose 不会更新版本键。作为一种解决方法,您可以使用以下中间件。
schema.pre('findOneAndUpdate', function() {
const update = this.getUpdate();
if (update.__v != null) {
delete update.__v;
}
const keys = ['$set', '$setOnInsert'];
for (const key of keys) {
if (update[key] != null && update[key].__v != null) {
delete update[key].__v;
if (Object.keys(update[key]).length === 0) {
delete update[key];
}
}
}
update.$inc = update.$inc || {};
update.$inc.__v = 1;
});
选项:optimisticConcurrency
乐观并发 是一种策略,用于确保您要更新的文档在您使用 find()
或 findOne()
加载它时,以及您使用 save()
更新它时没有发生更改。
例如,假设您有一个 House
模型,其中包含 photos
列表和一个表示此房屋是否显示在搜索中的 status
。假设状态为 'APPROVED'
的房屋必须至少有两张 photos
。您可能将批准房屋文档的逻辑实现为如下所示
async function markApproved(id) {
const house = await House.findOne({ _id });
if (house.photos.length < 2) {
throw new Error('House must have at least two photos!');
}
house.status = 'APPROVED';
await house.save();
}
markApproved()
函数在隔离状态下看起来很正确,但可能存在潜在问题:如果另一个函数在 findOne()
调用和 save()
调用之间删除了房屋的照片会怎样?例如,以下代码将成功执行
const house = await House.findOne({ _id });
if (house.photos.length < 2) {
throw new Error('House must have at least two photos!');
}
const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();
// Marks the house as 'APPROVED' even though it has 0 photos!
house.status = 'APPROVED';
await house.save();
如果您在 House
模型的架构上设置了 optimisticConcurrency
选项,则上述脚本将抛出错误。
const House = mongoose.model('House', Schema({
status: String,
photos: [String]
}, { optimisticConcurrency: true }));
const house = await House.findOne({ _id });
if (house.photos.length < 2) {
throw new Error('House must have at least two photos!');
}
const house2 = await House.findOne({ _id });
house2.photos = [];
await house2.save();
// Throws 'VersionError: No matching document found for id "..." version 0'
house.status = 'APPROVED';
await house.save();
选项:collation
为每个查询和聚合设置默认 排序规则。 这是一篇关于排序规则的入门级概述。
const schema = new Schema({
name: String
}, { collation: { locale: 'en_US', strength: 1 } });
const MyModel = db.model('MyModel', schema);
MyModel.create([{ name: 'val' }, { name: 'Val' }]).
then(() => {
return MyModel.find({ name: 'val' });
}).
then((docs) => {
// `docs` will contain both docs, because `strength: 1` means
// MongoDB will ignore case when matching.
});
选项:timeseries
如果您在架构上设置了 timeseries
选项,Mongoose 将为从该架构创建的任何模型创建 时间序列集合。
const schema = Schema({ name: String, timestamp: Date, metadata: Object }, {
timeseries: {
timeField: 'timestamp',
metaField: 'metadata',
granularity: 'hours'
},
autoCreate: false,
expireAfterSeconds: 86400
});
// `Test` collection will be a timeseries collection
const Test = db.model('Test', schema);
选项:skipVersioning
skipVersioning
允许将路径排除在版本控制之外(即,即使这些路径更新,内部修订版也不会递增)。除非您知道自己在做什么,否则不要这样做。对于子文档,请使用完全限定路径在父文档上包含此选项。
new Schema({ /* ... */ }, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented
选项:timestamps
timestamps
选项告诉 Mongoose 为您的架构分配 createdAt
和 updatedAt
字段。分配的类型是 Date。
默认情况下,字段的名称为 createdAt
和 updatedAt
。通过设置 timestamps.createdAt
和 timestamps.updatedAt
自定义字段名称。
timestamps
在幕后工作的方式是
- 如果您创建了一个新文档,Mongoose 仅会将
createdAt
和updatedAt
设置为创建时间。 - 如果您更新了文档,Mongoose 会将
updatedAt
添加到$set
对象中。 - 如果您在更新操作中设置了
upsert: true
,Mongoose 将使用$setOnInsert
操作符将createdAt
添加到文档中,以防upsert
操作导致插入了新的文档。
const thingSchema = new Schema({ /* ... */ }, { timestamps: { createdAt: 'created_at' } });
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing();
await thing.save(); // `created_at` & `updatedAt` will be included
// With updates, Mongoose will add `updatedAt` to `$set`
await Thing.updateOne({}, { $set: { name: 'Test' } });
// If you set upsert: true, Mongoose will add `created_at` to `$setOnInsert` as well
await Thing.findOneAndUpdate({}, { $set: { name: 'Test2' } });
// Mongoose also adds timestamps to bulkWrite() operations
// See https://mongoose.node.org.cn/docs/api/model.html#model_Model-bulkWrite
await Thing.bulkWrite([
{
insertOne: {
document: {
name: 'Jean-Luc Picard',
ship: 'USS Stargazer'
// Mongoose will add `created_at` and `updatedAt`
}
}
},
{
updateOne: {
filter: { name: 'Jean-Luc Picard' },
update: {
$set: {
ship: 'USS Enterprise'
// Mongoose will add `updatedAt`
}
}
}
}
]);
默认情况下,Mongoose 使用 new Date()
获取当前时间。如果您想覆盖 Mongoose 用于获取当前时间的函数,您可以设置 timestamps.currentTime
选项。Mongoose 会在需要获取当前时间时调用 timestamps.currentTime
函数。
const schema = Schema({
createdAt: Number,
updatedAt: Number,
name: String
}, {
// Make Mongoose use Unix time (seconds since Jan 1, 1970)
timestamps: { currentTime: () => Math.floor(Date.now() / 1000) }
});
选项:pluginTags
Mongoose 支持定义全局插件,这些插件应用于所有模式。
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
});
有时,您可能只想将给定的插件应用于某些模式。在这种情况下,您可以在模式中添加 pluginTags
const schema1 = new Schema({
name: String
}, { pluginTags: ['useMetaPlugin'] });
const schema2 = new Schema({
name: String
});
如果您使用 tags
选项调用 plugin()
,Mongoose 仅会将该插件应用于在 pluginTags
中具有匹配条目的模式。
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });
选项:selectPopulatedPaths
默认情况下,Mongoose 会自动为您 select()
所有填充的路径,除非您明确地将其排除在外。
const bookSchema = new Schema({
title: 'String',
author: { type: 'ObjectId', ref: 'Person' }
});
const Book = mongoose.model('Book', bookSchema);
// By default, Mongoose will add `author` to the below `select()`.
await Book.find().select('title').populate('author');
// In other words, the below query is equivalent to the above
await Book.find().select('title author').populate('author');
要选择默认情况下不选择填充的字段,请在您的模式中将 selectPopulatedPaths
设置为 false
。
const bookSchema = new Schema({
title: 'String',
author: { type: 'ObjectId', ref: 'Person' }
}, { selectPopulatedPaths: false });
const Book = mongoose.model('Book', bookSchema);
// Because `selectPopulatedPaths` is false, the below doc will **not**
// contain an `author` property.
const doc = await Book.findOne().select('title').populate('author');
选项:storeSubdocValidationError
出于历史原因,当单一嵌套模式的子路径中发生验证错误时,Mongoose 会记录单一嵌套模式路径中也发生了验证错误。例如
const childSchema = new Schema({ name: { type: String, required: true } });
const parentSchema = new Schema({ child: childSchema });
const Parent = mongoose.model('Parent', parentSchema);
// Will contain an error for both 'child.name' _and_ 'child'
new Parent({ child: {} }).validateSync().errors;
将子模式上的 storeSubdocValidationError
设置为 false
,使 Mongoose 仅报告父错误。
const childSchema = new Schema({
name: { type: String, required: true }
}, { storeSubdocValidationError: false }); // <-- set on the child schema
const parentSchema = new Schema({ child: childSchema });
const Parent = mongoose.model('Parent', parentSchema);
// Will only contain an error for 'child.name'
new Parent({ child: {} }).validateSync().errors;
选项:collectionOptions
选项如 collation
和 capped
会影响 Mongoose 在创建新集合时传递给 MongoDB 的选项。Mongoose 模式支持大多数 MongoDB createCollection()
选项,但并非所有选项。您可以使用 collectionOptions
选项设置任何 createCollection()
选项;Mongoose 将在为您的模式调用 createCollection()
时使用 collectionOptions
作为默认值。
const schema = new Schema({ name: String }, {
autoCreate: false,
collectionOptions: {
capped: true,
max: 1000
}
});
const Test = mongoose.model('Test', schema);
// Equivalent to `createCollection({ capped: true, max: 1000 })`
await Test.createCollection();
选项:autoSearchIndex
类似于 autoIndex
,但用于自动创建您在模式中定义的任何 Atlas 搜索索引。与 autoIndex
不同,此选项默认为 false。
const schema = new Schema({ name: String }, { autoSearchIndex: true });
schema.searchIndex({
name: 'my-index',
definition: { mappings: { dynamic: true } }
});
// Will automatically attempt to create the `my-index` search index.
const Test = mongoose.model('Test', schema);
选项:readConcern
读取关注 类似于 writeConcern
,但用于读取操作,如 find()
和 findOne()
。要设置默认 readConcern
,请将 readConcern
选项传递给模式构造函数,如下所示。
const eventSchema = new mongoose.Schema(
{ name: String },
{
readConcern: { level: 'available' } // <-- set default readConcern for all queries
}
);
使用 ES6 类
模式具有一个 loadClass()
方法,您可以使用它从 ES6 类 创建 Mongoose 模式
以下是如何使用 loadClass()
从 ES6 类创建模式的示例
class MyClass {
myMethod() { return 42; }
static myStatic() { return 42; }
get myVirtual() { return 42; }
}
const schema = new mongoose.Schema();
schema.loadClass(MyClass);
console.log(schema.methods); // { myMethod: [Function: myMethod] }
console.log(schema.statics); // { myStatic: [Function: myStatic] }
console.log(schema.virtuals); // { myVirtual: VirtualType { ... } }
可插拔
模式也是 可插拔的,这使我们能够将可重用功能打包成插件,这些插件可以与社区共享,也可以仅在您的项目之间共享。
进一步阅读
这里有一个 Mongoose 模式介绍的另一种方法。
要充分利用 MongoDB,您需要学习 MongoDB 模式设计的 basics。SQL 模式设计(第三范式)旨在 最小化存储成本,而 MongoDB 模式设计则是为了使常见的查询尽可能快。 MongoDB 模式设计的 6 个经验法则 博客系列 是学习使查询速度变快的基本规则的绝佳资源。
希望在 Node.js 中掌握 MongoDB 模式设计的用户应该研究 Christian Kvalheim(MongoDB Node.js 驱动程序 的最初作者)的 小型 MongoDB 模式设计手册。本书向您展示了如何为各种用例(包括电子商务、维基和预约预订)实现高性能模式。
下一步
现在我们已经介绍了 Schemas
,让我们看一下 SchemaTypes。