Schemas
如果你还没有尝试过 Mongoose,请先花一份中阅读 quickstart 去了解一下 Mongoose 是如何工作的。
如果你正在从 6.x 迁移到 7.x 请花一些时间阅读 migration guide.
- Defining your schema
- Creating a model
- Ids
- Instance methods
- Statics
- Query Helpers
- Indexes
- Virtuals
- Aliases
- Options
- With ES6 Classes
- Pluggable
- Further Reading
Defining your schema
Mongoose 中的所有内容都始于 Schema,每个 Schema 映射到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
属性,它将被转换为 SchemaType 中的 String
,data
属性将被转换为 SchemaType 中的 Date
要注意的是,如果属性只需要一个类型,则可以使用速记表示法指定它(将上面的 title 属性与 dateproperty 进行对比)。
还可以为键分配包含更多键/类型定义的嵌套对象,如上面的元属性。 每当键值是没有类型属性的 POJO 时,就是这种情况。
在这种情况下,Mongoose 只会为结构树中的叶子节点创建真实的 schema 路径(诸如上面的 meta.votes
和 meta.favs
)并且枝节将不会有真实的路径。一个副作用是 meta
将无法拥有自己的验证。如果需要验证,这个路径必须是被创建在节点树上的。另外阅读一下SchemaTypes guide 的 Mixed 小节了解一些陷阱。
允许的SchemaTypes 有:
了解更多请阅读 SchemaTypes here
Schemas 不仅仅定义文档的结构h和数学类型,它们也定义了 document methods, static Model methods, compound indexes, 和 document 生命周期钩子叫做 中间件(middleware).
创建模型
为了使用我们定义好的 Schema,我们需要把我们的 blogSchema
转化为能使用的 Model 。
为了这么做,,我们将它传递给 mongoose.model(modelName, schema)
:
const Blog = mongoose.model('Blog', blogSchema);
// ready to go!
Ids
默认的,Mongoose 会为你的 schemas 加上一个 _id
属性。
const schema = new Schema();
schema.path('_id'); // ObjectId { ... }
当你创建一个新的文档时,Mongoose 会为其会自动的添加 ObjectId类型的 _id
属性。
const Model = mongoose.model('Test', schema);
const doc = new Model();
doc._id instanceof mongoose.Types.ObjectId; // true
你也可以用自己的 _id
覆盖 Mongoose 的默认 _id
。要注意的是,如果一个文档不具有 _id
属性,Mongoose将不允许保存文档,因此你有必要设置 _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
Instance methods
`Models` 的实例是 documents。文档具有很多自己的 [built-in instance methods](api/document.html) 。 我们也可以自定义一些实例方法。// 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 文档的默认方法可能会导致不可预料的后果。See this for more details.
- 以上的例子直接使用了
Schema.methods
去保存一个实例方法。你也可以按 here 所描述的使用Schema.method()
helper。 - 不要用 ES6 的箭头函数声明方法。箭头函数明确地组织了绑定
this
,因此你的方法将无法访问到文档,相应的以上的示例将全部失效。
Statics
你也可以为模型添加静态方法。这里是一些等效的添加静态方法的途径:- 添加一个方法属性到 the schema-constructor (
statics
) 的第二个参数 - 添加一个方法属性到
schema.statics
- 调用
Schema#static()
function
// 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
,上面的示例将会失效。
Query Helpers
你也可以添加查询助手方法,看上去像实例方法但是对于 mongoose 查询的,查询助手方法使得你能去拓展 mongoose 的链式查询建造者(chainable query builder 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);
});
Indexes
MongoDB 支持第二索引(secondary indexes)。在 mongoose 中,我们可以通过 Schema
的 at the path level 或者 schema
level 设置索引。当创建组合索引时,在 schema level 设置索引是非常有必要的。
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() 了解其它的 index 选项。
当你的应用启动后,Mongoose 自动为每个 schema 中定义的索引调用 createIndex。Mongoose将会有序得创建每个索引,并且当所有的索引被成功创建无误,会 emit 一个 “index” 事件。
不过为了更好的生产,建议在生产环境下禁用这个行为,因为索引创建可能会导致显著的性能影响。
为了禁用这个行为,可以在 schema 中设置 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
事件在 model 上当索引被创建完成或者出现错误。
// 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 方法.
Virtuals
[虚拟(Virtuals)](api/schema.html#schema_Schema-virtual) 是指你可以从文档中获取到,但实际并不存储在 MongoDB 中的那些属性。getters 非常有助于 fields 的格式化,而setters对于将单个值组合成多个值以进行存储很有用。// 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' }
});
假设你想要打印全名,你可以这样实现:
Suppose you want to print out the person’s full name. You could do it yourself:
console.log(axl.name.first + ' ' + axl.name.last); // Axl Rose
但是每次都要拼接姓和名非常麻烦。那如果您想对名称进行一些额外的处理,例如删除附加符号呢?一个 虚拟属性获取器(virtual property getter) 能让你定义一个虚拟的全名属性,并且全名并不会被持久化地存储到 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 将默认地不会把虚拟属性包含在内,在使用 toJSON()
或 toObject()
时传递 { virtuals: true }
可以包含虚拟属性。
// Convert `doc` to a POJO, with virtuals attached
doc.toObject({ virtuals: true });
// Equivalent:
doc.toJSON({ virtuals: true });
对于 JSON.stringify()
也是一样的,如果你想让 JSON.stringify
的输入包含虚拟字段,可以调用 toObject({vitruals: true})
,也可以在定义 Schema 时直接设置 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
虚拟字段设置 setter,去同时更新姓和名。
// 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"
虚拟属性的 setters 在其它的验证前起效,因此上面的示例仍然有效当 first
和 last
都设置为必要时。
只有非虚拟的属性可以作为查询的字段,因为虚拟属性是不存在于 MongoDB 中的,我们不能依据它们去查询。
你可以 learn more about virtuals here.
Aliases
别名是一种特殊的虚拟,它的 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
作为别名即可。
[require:gh-6671]
Options
Schemas 有一些可供配置的选项被传递给构造器或者 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
option: autoIndex
默认情况下,在 Mongoose 连接到 MongoDB 后,Mongoose's `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);
来更改它。
option: autoCreate
在 Mongoose 创建索引之前,它会默认调用 `Model.createCollection()` 去创建 MongoDB 种的底层集合。 调用 `Model.createCollection()` 会根据排序规则选项(排序规则)设置集合默认排序规则,如果设置了上限架构选项(caped),则将集合建立为上限集合。您可以通过使用 mongoose.set('autoCreate', false)
将 autoCreate
设置为 false
来禁用此行为。与 autoIndex 一样,autoCreate 对开发和测试环境很有帮助,但您可能希望在生产环境中禁用它以避免不必要的数据库调用。
不幸的是,createCollection()
无法更改现有的集合。例如,如果将 capped: { size: 1024 }
添加到 schema 中,并且现有集合没有上限,则 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();
option: bufferCommands
默认情况下,mongoose 会在连接断开时缓冲命令,直到驱动程序设法重新连接。若要禁用缓冲,请将 `bufferCommands` 设置为 `false`。const schema = new Schema({ /* ... */ }, { bufferCommands: false });
schema 的 bufferCommands
选项将覆盖全局的 bufferCommands
选项。
mongoose.set('bufferCommands', true);
// Schema option below overrides the above, if the schema option is set.
const schema = new Schema({ /* ... */ }, { bufferCommands: false });
option: 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 });
option: capped
Mongoose 支持 MongoDB 的 capped collection。要指定要封顶的底层 MongoDB 集合,请为集合的最大大小设置上限(以字节为单位)。new Schema({ /* ... */ }, { capped: 1024 });
如果要传递其他选项,例如 max,也可以将上限选项设置为对象。在这种情况下,必须显式传递 size 选项,这是必需的。
new Schema({ /* ... */ }, { capped: { size: 1024, max: 1000, autoIndexId: true } });
option: collection
默认情况下,Mongoose 通过将模型名称传递给 `utils.toCollectionName` 方法来生成集合名称。此方法将名称复数化。如果集合需要其他名称,请设置此选项。const dataSchema = new Schema({ /* ... */ }, { collection: 'data' });
option: discriminatorKey
当你定义了一个鉴别器,Mongoose会向你的 schema 添加一个路径,用于存储文档是哪个鉴别器的实例。默认情况下,猫鼬会添加 `__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'
option: excludeIndexes
当 `excludeIndexes` 被设置为 `true`,Mongoose 将不会创建来自子文档 schema 的索引。这个选项当且仅当 schema 被用于子文档时有效,Mongoose 会忽略这个选项如果它被用于顶级模型的 schema。默认值为 `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
});
option: id
默认情况下,Mongoose 为每个 Schema 分配一个 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
option: _id
默认情况下为每个 Schema 分配一个 `_id` 字段,如果它没有被传递到这个 Schema 的构造器种的话。分配的类别是 [ObjectId](api/schema.html#schema_Schema-Types),与 MongoDB 的默认行为一致。如果你不想你的 schema 中有 `_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
});
option: minimize
默认情况下,Mongoose 将通过删除空对象来实现"minimize"。通过调用 `Model.lean()` 可以进行压缩。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
option: read
允许在 schema 级设置 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
的别名也是允许的,因此我们不必键入 outsecondaryPreferred
并弄错拼写,我们可以简单地传递 sp
。
read
选项还允许我们指定 tag
集。这些告诉 driver 它应该尝试从副本集的哪些成员读取。在 here和 here阅读有关tag
集的更多信息。
注意:您也可以在连接时指定驱动程序读取首选项 策略(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);
option: writeConcern
允许在 schema 级别设置 write concern 。
const schema = new Schema({ name: String }, {
writeConcern: {
w: 'majority',
j: true,
wtimeout: 1000
}
});
option: shardKey
`shardKey` 选项用于 [sharded MongoDB architecture](https://www.mongodb.com/docs/manual/sharding/) 每个分片集合都有一个分片键,该键必须存在于所有插入/更新操作中。我们只需要将此模式选项设置为 `sameshard` 键,我们就完成了一切。new Schema({ /* ... */ }, { shardKey: { tag: 1, name: 1 } });
要注意 Mongoose 不会发送 shardcollection
命令给你,你必须自行配置分片。
option: 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
option: 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 } });
猫鼬有一个单独的 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);
option: toJSON
和 [toObject](#toObject) 选项是完全相同的,但仅当调用 [toJSON()](https://thecodebarbarian.com/what-is-the-tojson-function-in-javascript.html) 方法时才适用。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
选项,看这里 。
option: 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
选项,看这里 。
option: typeKey
默认情况下,如果在 Schema 中有一个键 `type`,mongoose 会将其解释为类型声明。// Mongoose interprets this as 'loc is a String'
const schema = new Schema({ loc: { type: String, coordinates: [Number] } });
但是,对于像 geoJSON 这样的应用程序,type 属性很重要。如果要控制查找类型声明的键猫鼬,请设置 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
option: 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
option: versionKey
`versionKey` 是 Mongoose 首次创建时在每个文档上设置的属性(`__v`)。此键值包含文档的[内部修订版](http://aaronheckmann.blogspot.com/2012/06/mongoose-v3-part-1-versioning.html)。`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' }
请注意,Mongooses 默认版本控制不是完全乐观的并发 解决方案。Mongooses 默认版本控制仅在数组上运行,如下所示。
// 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;
});
option: optimisticConcurrency
[乐观并发(Optimistic concurrency)](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) 是一种策略,用于确保您正在更新的文档在使用 `find()` 或 `findOne()` 加载文档时与使用 `save()` 更新文档时不会发生变化。例如,假设您有一个包含照片列表的 `House` 模型,以及表示此房屋是否显示在搜索中的 `status`。假设状态为 `'APPROVED'` 的房屋必须至少有两张照片。您可以实现批准内部文档的逻辑,如下所示: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
Model
的 Schema 上设置 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();
option: collation
为每个查询和聚合设置校验。[Here's a beginner-friendly overview of collations](http://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-34-collations).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.
});
option: timeseries
如果在 Schema 上设置 `timeseries` 选项,Mongoose 将为从该 schema 创建的任何 model 创建 [timeseries collection](https://www.mongodb.com/docs/manual/core/timeseries-collections/) 。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);
option: skipVersioning
`skipVersioning` 允许从版本控制中排除路径(即使更新了这些路径,内部修订版也不会递增)。除非你知道自己在做什么,否则不要这样做。对于子文档,请使用完全限定路径在父文档中包含这个。new Schema({ /* ... */ }, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented
option: timestamps
`timestamps` 选项告诉 Mongoose 将 `createdAt` 和 `updatedAt` 字段分配给你的 schema。其类型是 `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://mongoosejs.com/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) }
});
option: pluginTags
Mongoose 支持定义全局的插件,插件会被应用到所有的 schemas。
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
});
有时候,你可能只是想要给某些 schemas 应用插件。这种情况下,你可以给一个 schema 添加 plugnTags
选项。
const schema1 = new Schema({
name: String
}, { pluginTags: ['useMetaPlugin'] });
const schema2 = new Schema({
name: String
});
如果你使用 tags
选项调用了 plugn()
,Mongoose 只会将该插件应用于那些在 pluginTags
中具有匹配条目的 schemas 。
// Add a `meta` property to all schemas
mongoose.plugin(function myPlugin(schema) {
schema.add({ meta: {} });
}, { tags: ['useMetaPlugin'] });
option: 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');
若要默认选择不选择填充字段,请在你的 schemas 中将 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');
option: storeSubdocValidationError
出于遗留原因,当单个嵌套 schema 的子路径中存在验证错误时,Mongoose 也会记录单个嵌套 schema 路径中存在验证错误。例如:
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;
在子 schema 上将 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;
option: collectionOptions
像 `collation` 和 `capped` 这样的选项会影响到 Mongoose 在创建新集合时传递的选项。Mongoose schemas 支持大部分 [MongoDB `createCollection()` options](https://www.mongodb.com/docs/manual/reference/method/db.createCollection/),但不是全部。 你可以使用 `collectionOptions` 选项去设置任何的 `createCollection()` 选项;当为你的 schema 调用 `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();
With ES6 Classes
Schemas 拥有一个 loadClass()
method,这个方法可以让你使用 ES6 class 创建一个 Mongoose schema:
- ES6 class methods become Mongoose methods
- ES6 class statics become Mongoose statics
- ES6 getters and setters become Mongoose virtuals
这是一个使用 loadClass()
去创建一个源自 ES6 类的 schema 的例子:
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
Schemas 也是可插拔的,这允许我们将可重用的功能打包到插件中,将插件在社区或者你的项目中共享。
Further Reading
这个是 Mongoose schemas 的另一份介绍。
为了充分利用 MongoDB,你需要学习 MongoDB 模式设计的基础知识。SQL模式设计(第三范式)旨在最小化存储成本,而 MongoDB 模式设计是关于尽可能快地进行常见查询。MongoDB 模式设计的6条经验法则博客系列是学习快速查询的基本规则的绝佳资源。
希望在 Node.js 中掌握 MongoDB 模式设计的用户,应该看看 Christian Kvalheim 的 The Little MongoDB Schema Design Book,这本书告诉你如何为一系列用例实现高性能模式,包括电子商务,wikis 和 预约。
Next Up
现在我们已经介绍了 Schemas,让我们来看看 SchemaTypes 。