结构
如果你还没有这样做,请花一点时间阅读 快速开始 以了解 Mongoose 的工作原理。 如果你要从 6.x 迁移到 7.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
和属性 date
,前者将转换为 字符串 SchemaType,后者将转换为 Date
SchemaType。
请注意,如果属性仅需要类型,则可以使用简写符号来指定(将上面的 title
属性与 date
属性进行对比)。
还可以为键分配包含进一步键/类型定义的嵌套对象,如上面的 meta
属性。 只要键的值是不具有 type
属性的 POJO,就会发生这种情况。
在这些情况下,Mongoose 只为树中的叶子创建实际的结构路径。 (如上面的 meta.votes
和 meta.favs
),并且分支没有实际路径。 这样做的一个副作用是上面的 meta
不能有自己的验证。 如果需要在树上进行验证,则需要在树上创建路径 - 有关如何执行此操作的更多信息,请参阅 子文档 部分。 另请阅读 SchemaTypes 指南的 混合 小节以了解一些问题。
允许的 SchemaType 是:
了解有关 此处的结构类型 的更多信息。
结构不仅定义文档的结构和属性转换,还定义文档 实例方法、静态模型方法、复合索引 和称为 middleware 的文档生命周期钩子。
创建模型
要使用我们的结构定义,我们需要将 blogSchema
转换为我们可以使用的 模型。
为此,我们将其传递到 mongoose.model(modelName, schema)
:
const Blog = mongoose.model('Blog', blogSchema);
// ready to go!
识别号
默认情况下,Mongoose 会向你的结构添加 _id
属性。
const schema = new Schema();
schema.path('_id'); // ObjectId { ... }
当你使用自动添加的 _id
属性创建新文档时,Mongoose 会为你的文档创建一个新的 ObjectId 类型的 _id
。
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 });
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
实例方法
Models
的实例是 documents。 文档有很多自己的 内置实例方法。
我们还可以定义自己的自定义文档实例方法。
// 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 文档方法可能会导致不可预测的结果。 详细信息请参见 this。
- 上面的例子直接使用
Schema.methods
对象来保存实例方法。 你还可以按照 here 的描述使用Schema.method()
辅助程序。 - not 是否使用 ES6 箭头函数声明方法 (
=>
)。 箭头函数 明确阻止绑定this
,因此你的方法将 not 有权访问该文档,并且上面的示例将不起作用。
静态
你还可以向模型添加静态函数。 添加静态的等效方法有以下三种:
- 将函数属性添加到结构构造函数的第二个参数 (
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'));
not 使用 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
at the path level 或 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:pass@127.0.0.1:port/database', { autoIndex: false });
// or
mongoose.createConnection('mongodb://user:pass@127.0.0.1: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
但每次都输入名字和姓氏 concatenating 会很麻烦。
如果你想对名称(例如 删除变音符号)进行一些额外处理怎么办? 虚拟属性获取者 允许你定义不会持久保存到 MongoDB 的 fullName
属性。
// 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()`
});
你还可以向虚拟设备添加自定义设置器,以便你通过 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"
虚拟属性设置器在其他验证之前应用。 因此,即使需要 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' }
你还可以在嵌套路径上声明别名。 使用嵌套结构和 subdocuments 更容易,但你也可以内联声明嵌套路径别名,只要使用完整嵌套路径 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
选项:自动索引
默认情况下,成功连接到 MongoDB 后,Mongoose 的 init()
功能 通过调用 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);
更改此默认值
选项:自动创建
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();
选项:缓冲区命令
默认情况下,当连接断开时,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 });
选项:上限
Mongoose 支持 MongoDB capped 集合。 要将底层 MongoDB 集合指定为 capped
,请将 capped
选项设置为 bytes 中集合的最大大小。
new Schema({ /* ... */ }, { capped: 1024 });
如果你想传递 max 等其他选项,也可以将 capped
选项设置为对象。
在这种情况下,你必须显式传递 size
选项,这是必需的。
new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } });
选项:集合
默认情况下,Mongoose 通过将模型名称传递给 utils.toCollectionName
方法来生成集合名称。
此方法使名称复数。 如果你需要为集合使用不同的名称,请设置此选项。
const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });
选项:discriminatorKey
当你定义 discriminator 时,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
是 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
字段转换为字符串,或者在 ObjectIds 的情况下,返回其十六进制字符串。 如果你不希望将 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
如果未将某个字段传递给 结构 构造函数,Mongoose 默认会为每个结构分配一个 _id
字段。
分配的类型是 ObjectId,以与 MongoDB 的默认行为一致。 如果你根本不想将 _id
添加到你的结构中,你可以使用此选项禁用它。
你可以 only 在子文档上使用此选项。 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
});
选项:最小化
默认情况下,Mongoose 将通过删除空对象来建立 "minimize" 结构。
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
选项:阅读
允许在结构级别设置 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'
每个首选项的别名也是允许的,因此我们可以简单地传递 'sp',而不必键入“secondaryPreferred”并导致拼写错误。
读取选项还允许我们指定标签集。 这些告诉 driver 它应该尝试从副本集的哪些成员中读取。 了解有关标签集 here 和 here 的更多信息。
注意: 你还可以在连接时指定驱动程序读取首选项 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);
选项:写关注
允许在结构级别设置 写下关注。
const schema = new Schema({ name: String }, {
writeConcern: {
w: 'majority',
j: true,
wtimeout: 1000
}
});
选项:分片键
当我们有 分片 MongoDB 架构 时,使用 shardKey
选项。
每个分片集合都有一个分片键,该键必须存在于所有插入/更新操作中。 我们只需将此结构选项设置为相同的分片键即可完成。
new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } });
请注意,Mongoose 不会为你发送 shardcollection
命令。 你必须自己配置你的分片。
选项:严格
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
选项:严格查询
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
选项是查询过滤器的 just。
// 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
选项,请阅读 this。
选项:对象
文档有一个 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
选项,请阅读 this。
选项:类型键
默认情况下,如果你的结构中有一个带有键 '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
设置为 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
是 Mongoose 首次创建每个文档时设置的属性。 该键值包含文档的内部 revision。 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 的默认版本控制是 not,这是一个完整的 乐观并发 解决方案。 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 会 not 更新版本密钥。 作为解决方法,你可以使用以下中间件。
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;
});
选项:乐观并发
乐观并发 是一种策略,可确保你正在更新的文档在使用 find()
或 findOne()
加载文档和使用 save()
更新文档之间不会发生更改。
例如,假设你有一个包含 photos
列表的 House
模型,以及表示该房屋是否出现在搜索中的 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
选项,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
允许从版本控制中排除路径(即,即使更新这些路径,内部修订也不会增加)。 除非你知道自己在做什么,否则请勿这样做。 对于子文档,请使用完全限定路径将其包含在父文档中。
new Schema({ /* ... */ }, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented
选项:时间戳
timestamps
选项告诉 Mongoose 将 createdAt
和 updatedAt
字段分配给你的结构。 指定的类型是 日期。
默认情况下,字段名称为 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.nodejs.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) }
});
选项:插件标签
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'] });
选项:选择填充路径
默认情况下,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;
选项:集合选项
collation
和 capped
等选项会影响 Mongoose 在创建新集合时传递给 MongoDB 的选项。
Mongoose 结构支持大多数 MongoDB createCollection()
选项,但不是全部。
你可以使用 collectionOptions
选项来设置任何 createCollection()
选项; 当为你的结构调用 createCollection()
时,Mongoose 将使用 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();
使用 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 { ... } }
插件化
结构也是 pluggable,它允许我们将可重用的功能打包到插件中,这些插件可以与社区或仅在你的项目之间共享。
进一步阅读
这是 Mongoose 结构的替代介绍。
为了充分利用 MongoDB,你需要学习 MongoDB 结构设计的基础知识。 SQL 结构设计(第三范式)是针对 最小化存储成本 设计的,而 MongoDB 结构设计是为了尽可能快地进行常见查询。 MongoDB 结构设计博客系列的 6 个经验法则 是学习快速查询基本规则的绝佳资源。
希望掌握 Node.js 中 MongoDB 结构设计的用户应该查看 MongoDB Node.js 驱动程序 的原作者 Christian Kvalheim 的 MongoDB 结构设计小书。 本书向你展示如何为一系列用例(包括电子商务、维基和预约)实现高性能结构。
下一步
现在我们已经介绍了 Schemas
,让我们来看看 SchemaTypes。