Mongoose
MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。
Mongoose是在node.js环境下对mongodb进行便捷操作的对象模型工具
快速上手
请先安装 MongoDB 和 Node.js。
下一步, npm
安装Mongoose:
npm i mongoose
连接数据库test
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
connect()
返回一个状态待定(pending)的连接,接着我们加上成功和失败的提醒:
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', function () {
// ..
});
连接成功的时候,回调函数会被调用,假设下面所有函数都在这个回调函数里面。
Mongoose 里面一切始于 Schema。看一个简单的例子:
const KittySchema = mongoose.Schema({
name: String
});
我们得到了一个带有 String
类型的 name
属性的 schema, 接着把这个 schema 编译成一个 Model:
const Kitten = mongoose.model('Kitten', kittySchema);
model 是我们创造的 document 的 Class。在例子中,每个 document 都是一个实例,它的属性和行为都会被声明在 schema 。
const felyne = new Kitten({ name: 'Felyne' });
felyne.name; // 'Felyne';
// 给 document 加一个 speak 方法
KittySchema.methods.speak = function () {
const greeting = this.name ? 'Meow name is ' + this.name : "I don't have a name";
console.log(greeting);
};
const Kitten = mongoose.model('Kitten', kittySchema);
加在 schema 的 methods
属性的函数会编译到 Model
的prototype, 也会暴露搭配每个 document 实例:
const a = new Kitten({
name: 'A'
});
a.speak(); // Meow name is A
每个 document 会在调用它的 save 方法后保存到数据库中,注意回调函数的第一个参数永远是 error
A.save(function (err, A) {
if (err) return console.error(err);
A.speak();
});
如果有很多个实例,可以用下面的方法来获取 model 里面的所有数据
Kitten.find(function (err, kittens) {
if (err) return console.error(err);
console.log(kittens);
});
模式(Schemas)
定义一个schema
Mongoose 的一切始于 Schema, 每个 schama 都会映射到一个 MongoDB collection,并定义这个 collection 里面的文档的构成。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const blog = new Schema({
title: String,
author: String,
body: String,
comments: [
{
body: String,
date: Date
}
],
date: {
type: Date,
defalut: Date.now
},
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
document 里面的每个属性都会被转换成在 blogSchema 里面对应的 SchemaType。例如 title
属性会被转换成为 SchemaType String, 而 date
属性会被转换成为 SchemaType Date。还可以像 meta
属性一样,更加详细地指定嵌套在里面的属性类型。
运行使用的 SchemaTypes 有:
- 'String'
- 'Number'
- 'Boolean'
- 'Date'
- 'Buffer'
- 'Mixed'
- 'ObjectId'
- 'Array'
Schema 的功能不只是定义文档结构和属性类型,还可以定义
- 'document 的 instance methods'
- 'model 的 static Model methods'
- '复合索引'
- '文档的生命周期钩子,也称为中间件'
创建一个 model
mongoose.model(modelName,schema)
函数把 schema 转换为一个 Model:
const Blog = mongoose.model('Blob', blogSchema);
实例方法(method)
document 是 Models 的实例。Document 有很多自带的实例方法,当然也可以自定义我们自己的方法
// 定义一个 schema
const animalSchema = new Schema({
name: String,
type: String
});
animalSchema.methods.findSimilarTypes = function (cb) {
return this.model('Animal').find(
{
type: this.type
},
cb
);
};
现在所有的 animal
实例都有 findSimilarTypes
方法:
const Animal = mongoose.model('Animal', animalSchema);
const dog = new Animal({
type: 'dog'
});
dog.findSimilarTypes(function(err, dogs) {
console.log(dogs); // woof
});
- '重写 mongoose 的默认方法会造成无法预料的结果'
- '不要在自定义方法中使用 ES6 箭头函数,会造成 this 指向错误'
静态方法(static)
添加 Model
的静态方法也十分简单,继续使用 animalSchema
举例:
animalSchema.statics.findByName = function (name, cb) {
return this.find(
{
name: new RegExp(name, 'i')
},
cb
);
};
const Animal = mongoose.model('Animal', animalSchema);
Animal.findByName('fido', function (err, animals) {
console.log(animals);
});
同样注意的是不要在静态方法中使用 ES6 的箭头函数
查询助手(query helper)
查询助手作用于 query 实例,方便自定义扩展链式查询
animalSchema.query.byName = function (name) {
return this.find({
name: new RegExp(name, 'i')
});
};
const Animal = mongoose.model('Animal', animalSchema);
Animal.find()
.byName('fido')
.exec(function (err, animal) {
console.log(animals);
});
索引(index)
MongoDB 支持 secondary indexes。在 mongoose 中,我们在 Schema 定义索引。索引分字段级别和 schema 级别,复合索引需要在 schema 级别定义。
const animalSchema = new Schema({
name: String,
type: String,
tags: {
type: [String],
index: true
}
});
animalSchema.index({
name: 1,
type: -1
});
应用启动的时候,Mongoose 会自动调用 createIndex
初始化定义的索引。Mongoose 顺序处理每一个 createIndex,然后在 model 触发 'index' 事件。在生产中建议禁止这种行为,因为创建索引会对性能造成重大影响。
mongoose.connect('mongodb://user:pass@localhost:port/database', {
autoIndex: false
});
// 或者
mongoose.createConnection('mongodb://user:pass@localhost:port/database', {
autoIndex: false
});
// 或者
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', function (err) {
// _id index cannot be sparse
console.log(err);
});
虚拟值(Virtual)
Virtual 是 document 的属性,但是不会被保存到 MongoDB.getter 可以用于格式化和组合字段数据,setter 可以很方便地分解一个值到多个字段。
const personSchema = new Schema({
name: {
first: String,
last: String
}
});
const Person = mongoose.model('Person', personSchema);
const axl = new Person({
name: {
first: 'Axl',
last: 'Rose'
}
});
console.log(axl.name.first + ' ' + axl.name.last); //Axl Rose
但是每次都要这样拼接就太麻烦了,使用 virtual property getter 这个方法允许定义一个 fullName 属性,但不必保存到数据库。
personSchema.virtual('fullName').get(function () {
return this.name.first + ' ' + this.name.last;
});
console.log(axl.fullName); // Axl Rose
如果对 document 使用 toJSON()
或 toObject()
默认不包括虚拟值,需要额外向 toObject 或者 toJSON 传入参数 {virtuals:true}
也可以设定虚拟值的 setter,下面的例子中,当赋值到虚拟值的时候,会自动拆分到其他属性
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"
需要注意的是,虚拟值不能用于查询和字段选择,因为虚拟值不存储在 MongoDB
别名(Alias)
Aliase 是一种特殊的虚拟值,它的getter 和 setter 会无缝链接到另一个值,这是一个节省带宽的做法,可以存储一个更短的属性名到数据库,同时在调用的时候保持可读性。
const personSchema = new Schema({
n: {
type: String,
alias: 'name'
}
});
const person = new Person({
name: 'Val'
});
person; //{n:'Val'}
person.toObject({
virtuals: true
}); //{n:'Val',name:'Val'}
person.name; // Val
person.name = 'Not Val';
person; // {n:'Not Val'}
选项
Schema 有很多可配置选项,可以在构造时传入或者直接 set
new Schema({
...
}, options);
const schema = new Schema({
...
});
schema.set(option, value);
有效的选项:
- 'autoIndex'
- 'bufferCommands'
- 'capped'
- 'collection'
- 'id'
- '_id'
- 'minimize'
- 'read'
- 'shardKey'
- 'strict'
- 'strictQuery'
- 'toJSON'
- 'toObject'
- 'typekey'
- 'validateBeforeSave'
- 'versionKey'
- 'skipVersioning'
- 'timestamps'
- 'useNestedStrict'
option:autoIndex
应用启动的时候,Mongoose 自动发送 createIndex
指令,schema 里面的每个 index 都会被创建,如果需要关闭自动创建功能或者需要在创建后进行一系列操作,可以把 autoIndex
设为 false
,然后对 model 调用 ensureIndexes:
const schema = new Schema({
..
}, {
autoIndex: false
});
const Clock = mongoose.model('Clock', schema);
Clock.ensureIndexes(callback);
option:bufferCommands
默认情况下,mongoose buffers会在连接中断时发出命令,直到驱动程序重新连接为止。若要禁用缓冲,请将bufferCommands设置为false。
const schema = new Schema({
..
}, {
bufferCommands: false
});
schema 的bufferCommands 会重写全局的 bufferCommands 选项。
mongoose.set('bufferCommands',true);
const schema = new Schema({
..
}, {
bufferCommands: false
});
option:capped
Mongoose 支持 MongoDB 的 capped collections ,要从底层把 collection 设定为 capped(封顶),可以把collection 的最大容量设定到 capped
选项(单位 bytes)
new Schema({
..
}, {
capped: 1024
});
如果要传递多个选项,capped 也可以作为对象传递. 这个试试需要显式传入必要值 size
new Schema({
..
}, {
size: 1024,
max: 1000,
autoIndexId: true
})
option:collection
Mongoose 通过 utils.toCollectionName 方法,默认生成 collection 的名称(生成 model 名称的复数形式)。设置这个选项可以自定义名称
const dataSchema = new Schema({
..
}, {
collection: 'data'
});
option:id
Mongoose 会默认生成一个虚拟值 id
,指向文档的 _id
字段。如果你不需要 id
虚拟值,可以通过这个选项禁用此功能。
// 默认行为
const schema = new Schema({
name: String
});
const Page = mongoose.model('Page', schema);
const p = new Page({
name: 'mongodb.org'
});
console.log(p.id); // 50341373e894ad16347efe01
//禁止 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
Mongoose 默认给你的 Schema 赋值一个 _id
,这个值的类型是 ObjectId,这与 MongoDB 的默认表现一致。如果需要,可以禁止
此选项只能用于 subdocument。 Mongoose 不能保存没有id的文档,如果硬是要这么做,会报错。
// 默认行为
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' }
// 禁止 _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); // { name: 'mongodb.org' }
option:minimize
Mongoose 默认不保存空对象。
const schema = new Schema({
name: String,
inventory: {}
});
const Character = mongoose.model('Character', schema);
const frodo = new Character({
name: 'Frodo',
inventory: {
ringOfPower: 1
}
});
Character.findOne(
{
name: 'Frodo'
},
function (err, character) {
console.log(character); // {name: 'Frodo', inventory: { ringOfPower: 1 }}
}
);
const Sam = new Character({
name: 'Sam',
inventory: {}
});
Character.findOne(
{
name: 'Sam'
},
function (err, character) {
console.log(character); // {name: 'Sam'}
}
);
如果把 minimize 设定为 false, Mongoose 将保存空对象
const Sam = new Character(
{
name: 'Sam',
inventory: {}
},
{
minimize: false
}
);
Character.findOne(
{
name: 'Sam'
},
function (err, character) {
console.log(character); // {name: 'Sam',inventory:{}}
}
);
option:read
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'
const options = {
replset: {
strategy: 'ping'
}
};
mongoose.connect(uri, options);
const schema = new Schema({
..
}, {
read: ['nearest', {
disk: 'ssd'
}]
});
mongoose.model('JellyBean', schema);
option:shardKey
分片相关.
new Schema({
..
}, {
shardKey: {
tag: 1,
name: 1
}
})
option:strict
Strict 选项默认为 true,这意味着你不能 save
schema 里没有声明的属性。
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
这个值可以在 model 级别重写,在第二个参数值传入:
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”,这将导致产生错误,而不是删除坏数据。
注意: 在 schema 中不存在的实例上设置的任何键/值总是被忽略,不管模式选项是什么。
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
const mySchema = new Schema(
{
field: Number
},
{
strict: true
}
);
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will **not** filter out `notInSchema: 1`, despite `strict: true`
MyModel.find({
notInSchema: 1
});
严格的选择适用于更新
// Mongoose will strip out `notInSchema` from the update if `strict` is not `false`
MyModel.updateMany(
{},
{
$set: {
notInSchema: 1
}
}
);
Mongoose 有一个单独的 strictQuery 选项,用于将过滤器参数的strict模式切换到查询。
const mySchema = new Schema(
{
field: Number
},
{
strict: true,
strictQuery: true // Turn on strict mode for query filters
}
);
const MyModel = mongoose.model('Test', mySchema);
// Mongoose will strip out `notInSchema: 1` because `strictQuery` is `true`
MyModel.find({
notInSchema: 1
});
option:toJSON
与toObject选项完全相同,但仅在调用documents toJSON方法时才适用。
const schema = new Schema({
name: String
});
schema.path('name').get(function (v) {
return v + ' is my name';
});
schema.set('toJSON', {
getters: true,
virituals: false
});
const M = mongoose.model('Person', schema);
const m = new M({
name: 'Max Headroom'
});
m.toObject(); // {_id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom' }
m.toJSON(); // {_id: 504e0cd7dd992d9be2f20b6f, name: 'Max Headroom is my name' }
JSON.stringfy(m); // { "_id": "504e0cd7dd992d9be2f20b6f", "name": "Max Headroom is my name" }
option:toObject
Documents 的 toObject 方法可以把文档转换成一个 plain javascript object (也就是去掉里面的方法)。 这是一个可以接收多个参数的方法,我们可以在 schemas 定义这些参数。
例如要打印出虚拟值,可以向 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: 'HAHA'
});
m; // { _id: 504e0cd7dd992d9be2f20b6f, name: 'HAHA is my name' }
option:typekey
类型声明
const schema = new Schema({
loc: {
type: String,
coordinates: [Number]
}
});
对于像geoJSON这样的应用程序,“type”属性非常重要。控制使用哪个键mongoose来查找类型声明,要设置“typeKey”模式选项
const schema = new Schema(
{
loc: {
type: String,
coordinates: [Number]
},
name: {
$type: String
}
},
{
typeKey: '$type'
}
);
option:validateBeforeSave
默认情况下,文档在保存到数据库之前会自动验证。这是为了防止保存无效的文档。如果希望手动处理验证,并能够保存没有通过验证的对象,可以将validatebeforeave设置为false。
const schema = new Schema({
name: String
});
schema.set('validateBeforeSave', false);
schema.path('name').validate(function (value) {
return v != 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 在文件创建时自动设定的。 这个值包含文件的内部修订号。 versionKey
是一个字符串,代表版本号的属性名, 默认值为 __v
。如果这个值与你的计划冲突,你可以设定为其他名称:
const schema = new Schema({
name: 'string'
});
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({
name: 'mongoose v3'
});
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' }
你也可以赋值为 false
禁用 versionKey
。
const Schema({
..
}, {
versionKey: false
});
const Thing = mongoose.model('Thing', schema);
const thing = new Thing({
name: 'no versioning please'
});
thing.save(); // { name: 'no versioning please' }
option:collation
为 查询(query)和 聚合(aggregation)设置 collation
const schema = new Schema({
name: String
}, {
collaction: {
locale: 'en_US',
strength: 1
}
})
const MyModel = db.model('MyModel', schema);
myModel.create([{
name: 'val',
{
name: 'Val'
}]).then(function() {
return MyModel.find({
name: 'Val'
});
}).then(function(docs) {
// docs 里面会包含上面两个值,因为 strength:1 代表着忽略大小写
});
option:skipVersioning
跳过版本控制允许从版本控制中排除路径(例如。,即使更新了这些路径,也不会增加内部修订)。除非你知道你在做什么,否则不要这样做。对于子文档,使用完全限定路径将其包含在父文档中。
new Schema({
..
}, {
skipVersioning: {
dontVersionMe: true
}
});
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented
option:timestamps
如果设置了 timestamps
选项, mongoose 会在你的 schema 自动添加 createdAt
和 updatedAt
字段, 其类型为Date。这两个字段的默认名称是 createAt
和 updateAt
,你可以通过设定 timestamps.createAt
和 timestamps.updateAt
自定义名称。
const thingSchema = new Schema({
..
}, {
timestamps: {
createdAt: 'created_at'
}
});
const Thing = mongoose.model('Thing', thingSchema);
const thing = new Thing();
thing.save(); // `created_at` & `updatedAt` will be included
option:useNestedStrict
在 mongoose4 中, update()
和 findOneAndUpdate()
只检查顶层 schema 的严格模式设定。
const childSchema = new Schema(
{},
{
strict: false
}
);
const parentSchema = new Schema(
{
child: childSchema
},
{
strict: 'throw'
}
);
const Parent = mongoose.model('Parent', parentSchema);
Parent.update(
{},
{
'child.name': 'Luke Skywalker'
},
function (error) {
// 报错!原因是父Schema设定为`strict: throw`,但是因为只检查顶层,导致
// 子Schema的 `strict: false` 遭到无情忽视
}
);
const update = {
'child.name': 'Luke Skywalker'
};
const opts = {
strict: false
};
Parent.update({}, update, opts, function (error) {
// 这样可以,因为重写了父Schema的 strict 选项
});
如果你把 useNestedStrict
设为 true,mongoose 就不会忽略嵌套的 strict 设定。
const childSchema = new Schema(
{},
{
strict: false
}
);
const parentSchema = new Schema(
{
child: childSchema
},
{
strict: 'throw',
useNestedStrict: true
}
);
const Parent = mongoose.model('Parent', parentSchema);
Parent.update(
{},
{
'child.name': 'Luke Skywalker'
},
function (error) {
// Works!
}
);
模式类型(SchemaTypes)
SchemaTypes 是处理字段路径各种属性的定义(默认值、验证、getter、setter、查询的字段选择默认值,以及字符串和数字的其他一般特性)
下面是合法的 SchemaTypes:
- 'String'
- 'Number'
- 'Date'
- 'Buffer'
- 'Boolean'
- 'Mixed'
- 'ObjectId'
- 'Array'
- 'Decimal128'
例子:
const schema = new Schema({
name: String,
binary: Buffer,
living: Boolean,
updated: {
type: Date,
default: Date.now
},
age: {
type: Number,
min: 18,
max: 65
},
mixed: Schema.Types.Mixed,
_someId: Schema.Types.ObjectId,
decimal: Schema.Types.Decimal128,
array: [],
ofString: [String],
ofNumber: [Number],
ofDates: [Date],
ofBuffer: [Buffer],
ofBoolean: [Boolean],
ofMixed: [Schema.Types.Mixed],
ofObjectId: [Schema.Types.ObjectId],
ofArrays: [[]],
ofArrayOfNumbers: [[Number]],
nested: {
stuff: {
type: String,
lowercase: true,
trim: true
}
}
});
const Thing = mongoose.model('Thing', schema);
const m = new Thing();
m.name = 'haha';
m.age = 125;
m.updated = new Date();
m.binary = new Buffer(0);
m.mixed = {
any: {
thing: 'i want'
}
};
m.markModified('mixed');
m._someId = new mongoose.Typed.ObjectId();
m.array.push(1);
m.ofString.push('strings!');
m.ofNumber.unshift(1, 2, 3, 4);
m.ofDates.addToSet(new Date());
m.ofBuffer.pop();
m.ofMixed = [
1,
[],
'three',
{
four: 5
}
];
m.nested.stuff = 'good';
m.save(callback);
SchemaType 选项
可以直接声明 schema type 为某一种 type,或者赋值一个含有 type 属性的对象
const schema = new Schema({
test: String
});
const schema2 = new Schema({
test: {
type: String
}
});
除了 type 属性,还可以对这个字段路径指定其他属性,如果要在保存之前把字母都改成小写:
const schema2 = new Schema({
test: {
type: String,
lowercase: true
}
});
lowercase
属性只作用于字符串。以下有一些全部type 可用的选项和一些限定部分 type 使用的选项。
全部可用
- '
required
:布尔值或者函数,如果值为真,为此属性添加 required 验证器' - '
default
:任何值或者函数设置此路径默认值,如果是函数,函数返回值为默认值' - '
select
:布尔值执行query 默认 projections' - '
validate
:属性验证函数' - '
get
:函数 使用Object.defineProperty()
定义自定义 getter' - '
set
:函数 使用Object.defineProperty()
定义自定义 setter' - '
alias
:字符串 仅mongoose >= 4.10.0。 为该字段路径定义虚拟值 gets/sets'
const numberSchema = new Schema({
integerOnly: {
type: Number,
get: v => Math.round(v),
set: v => Math.round(v),
alias: 'i'
}
});
const Number = mongoose.model('Number', numberSchema);
const doc = new Number();
doc.integerOnly = 2.001;
doc.integerOnly; // 2
doc.i; // 2
doc.i = 3.001;
doc.integerOnly; //3
doc.i; //3
索引相关
可以使用 schema type 选项定义 MongoDB indexex
- '
index
: 布尔值 是否对这个属性创建索引' - '
unique
: 布尔值 是否对这个属性创建唯一索引' - '
sparse
: 布尔值 是否对这个属性创建稀疏索引'
const schema = new Schema({
test: {
type: String,
index: true,
unique: true
}
});
String
- '
lowercase
: 布尔值 是否保存前对此值调用.toLowerCase()
' - '
uppercase
: 布尔值 是否保存前对此值调用.toUpperCase()
' - '
trim
: 布尔值 是否在保存前对此值调用trim()
' - '
match
: 正则表达式 创建验证器检查这个值是否匹配给定正则表达式' - '
enum
: 数组 创建验证器检查这个值是否包含于给定数组'
Number
- '
min
: 数值 创建验证器检查属性是否大于或者等于该值' - '
max
: 数值 创建验证器检查属性是否小于或者等于该值'
Date
- '
min
: Date' - '
max
: Date'
使用注意
Dates
内建的 Date 方法不会触发 mongoose 修改跟踪逻辑,如果使用 setMonth()
修改文档里面的 Date
, mongoose 在 doc.save()
的时候是察觉不到这个文档发生了变化的,因此保存不到数据库中。如果一定要用内建的 Date 方法,需要手动调用 doc.markModified('pathToYourDate')
告诉 mongoose 修改了数据。
const Assignment = mongoose.model('Assignment', {
dueDate: Date
});
Assignment.findOne(function (err, doc) {
doc.dueDate.setMonth(3);
doc.save(callback); // 这个不会保存你的修改
doc.markModified('dueDate');
doc.save(callback);
});
Mixed
一个什么都可以放的 SchemaType, 虽然遍历,但是也会让数据难以维护。Mixed 可以通过 Schema. Types. Mixed 或者传入 一个空对象定义。下面三种方法效果是一致的。
const Any = new Schema({
any: {}
});
const Any = new Schema({
any: Object
});
const Any = new Schema({
any: Schema.Types.Mixed
});
因为这是个 schema-less type,所以可以赋值为任意类型,但是 mongoose 无法自动检测并保存修改。要告诉它修改了 Mixed type 的值,调用文档的 .markModified(path)
方法,传入你的 Mixed 字段路径
person.anything = {
x: [
3,
4,
{
y: 'changed'
}
]
};
person.markModified('anything');
person.save(); // anything will now get changed
ObjectIds
要指定类型为 ObjectId,在声明中使用 Schema.Types.ObjectId
const mongoose = require('mongoose');
const ObjectId = mongoose.Schema.Types.ObjectId;
const Car = new Schema({
driver: ObjectId
});
Arrays
创造 SchemaTypes 或者子文档数组
const ToySchema = new Schema({
name: String
});
const ToyBox = new Schema({
toys: [ToySchema],
buffers: [Buffer],
string: [String],
number: [Number]
});
注意:指定空数组相当于 Mixed
, 下面的操作相当于创建 Mixed
数组:
const Empty1 = new Schema({
ary: {
]
}
});
const Empty2 = new Schema({
any: Array
});
const Empty3 = new Schema({
any: [Schema.Types.Mixed]
});
const Empty4 = new Schema({
any: [{}]
});
数组的默认值是 []
(空数组)
const Toy = mongoose.model('Test', toySchema);
new Toy().toys; // []
要手动把默认值设置为 undefined
,从而覆盖 []
const ToySchema = new Schema({
toys: {
type: [ToySchema],
defalut: undefined
}
});
创建自定义 Type
Mongoose 可以扩展自定义 SchemaType.
** schema.path()
函数**
这个函数为给定字段路径返回实例化 schema type
const sampleSchema = new Schema({
name: {
type: String,
require: true
}
});
sampleSchema.path('name');
/**
* SchemaString {
* enumValues: [],
* regExp: null,
* path: 'name',
* instance: 'String',
* validators: ...
*/
这个函数可以检查给定字段路径的检查器和类型等信息。
连接(Connections)
可以使用 mongoose.connect()
方法连接 MongoDB
mongoose.connect('mongodb://localhost/myapp');
这是连接本地 myapp
数据库默认接口(27017)的最小配置。本地连接失败可以尝试 127.0.0.1.local hostname 被修改有时候会引起问题。
可以在 url 中指定多个参数:
mongoose.connect('mongodb://username:password@host:port/database?options');
操作缓存
不必等等连接建立成功就可以使用 Mongoose models
mongoose.connect('mongodb://localhost/myapp');
const MyModel = mongoose.model(
'Test',
new Schema({
name: String
})
);
MyModel.findOne(function (err, res) {
// ...
});
Mongoose 会缓存 model 操作。这个操作很方便,但也会引起一些疑惑,因为如果没有连上的话,Mongoose 不会抛错。
const MyModel = mongoose.model(
'Test',
new Schema({
name: String
})
);
MyModel.findOne(function (err, res) {
// ..
});
setTimeout(function () {
mongoose.connect('mongodb://localhost/xxx');
}, 6000);
要禁用缓存,修改 bufferCommands 配置。如果打开了这个选项连接被挂起,尝试关闭 bufferCommands 检查是否正确打开连接,也可以全局禁用 bufferCommands:
mongoose.set('bufferCommands', false);
选项
connect
方法也接受 options
参数,这些参数会传入底层 MongoDB 驱动
mongoose.connect(uri, options);
Mongoose 会不做修改直接把选项传到驱动,以下有一点例外
- '
bufferCommands
是 mongoose 特有的选项,' - '
user/pass
用于认证的用户名和密码。mongoose 特有的,等价于 MongoDB 驱动的auth.user
和auth.password
选项' - '
autoIndex
理想情况下,mongoose 在连接时会自动建立 schema 索引。这有利于开发,但是在大型生产环境中不是很理想,因为会导致性能下降。' - '
dbName
指定要连接的数据库名称(覆盖连接字符串)'
下面是一些重要的选项
- '
autoReconnect
底层 MongoDB 驱动在连接后将自动重连。除非是管理连接池的高手,不然不建议设置为 false' - '
bufferMaxEntries
MongoDB 驱动同样有自己的离线缓存机制。如果希望连接错误时终止数据库操作,可以设这个值为 0 以及把 bufferCommands 设为 false.' - '
promiseLibrary
设置底层 promise 库' - '
poolSize
MongoDB 保持的最大 socket 连接数。默认是5. MongoDB 3.4 之前,只允许每个 socket 同时进行一个操作,所以如果有几个缓慢请求卡着后面快的请求,可以尝试增加连接数。'
举例子:
const options = {
useMongoClient: true,
autoIndex: false, // 不要创建索引
reconnectTries: Number.MAX_VALUE, // 不要停止重连
reconnectInterval: 500, // 每500毫秒重连
poolSize: 10, // 维护最多10个socket
bufferMaxEntries: 0 // 如果没有连接,则立即返回错误,而不是等待重新连接
};
mongoose.connect(uri, options);
回调
connect
函数接受回调函数,或者返回一个 promise
mongoose.connect(uri, options, function(error) {
});
mongoose.connect(uri, options).then(
() => {
/** ready to use. The `mongoose.connect()` promise resolves to undefined. */ }
err => {
/** handle initial connection error */ }
);
连接字符串(Connectng String)选项
可以在连接字符串填写驱动选项,这只适用于 MongoDB 驱动使用的选项,所以类似 bufferCommands 的 Mongoose 专用选项不能在连接字符串使用
mongoose.connect('mongodb://localhost:27017/test?connectTimeoutMS=1000&bufferCommands=false');
// 相当于
mongoose.connect('mongodb://localhost:27017/test', {
connectTimeoutMS: 1000
// Note that mongoose will **not** pull `bufferCommands` from the query string
});
把选项放在连接字符串的劣势是不便阅读。优势是简短。最佳实践是把区分生产环境和开发环境的选项, socketTimeoutMS
、 connectTimeoutMS
放在 uri , 把通用的常量如 connectTimeoutMS
、 poolSize
放在选项对象里。
keepAlive注意事项
对于长期运行的后台引用,启用毫秒级 keepAlive
是一个精明的操作。不这么做偶尔会经常收到看似没有什么原因的 connection closed
错误
mongoose.connect(uri, {
keepAlive: 120
});
副本集(Replica Set)连接
要连接到副本集,可以使用逗号分隔开,传入多个地址:
mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]);
连接到单节点副本集,需要指定 relicaSet
选项
mongoose.connect('mongodb://host1:port1/?replicaSet=rsName');
多 mongos 支持
使用高性能分片集群,需要连接多个 mongos(MongoDB Shard) 实例。 在 mongoose 5.x 中, 你在连接多个 mongos 时不需要传入任何特殊选项。
// Connect to 2 mongos servers
mongoose.connect('mongodb://mongosA:27501,mongosB:27501', cb);
多个连接
之前我们了解如何使用 Mongoose 默认连接方法连接到 MongoDB。但有时候我们需要权限不同的多个连接, 或是连接到不同数据库。这个情况下我们可以使用 mongoose.createConnection()
, 它接受之前提到的所有参数,给你返回一个新的连接。
const conn = mongoose.createConnection(
'mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]',
options
);
connection对象后续用于创建和检索models。 models 的范围总是局限于单个连接。
调用 mongoose.connect()
时,Mongoose 会自动创建默认连接。 你可以使用 mongoose.connection
访问默认连接。
连接池
无论是使用 mongoose.connect
或是 mongoose.createConnection
创建的连接, 都被纳入默认最大为 5 的连接池,可以通过 poolSize 选项调整:
// With object options
mongoose.createConnection(uri, {
poolSize: 4
});
const uri = 'mongodb://localhost/test?poolSize=4';
mongoose.createConnection(uri);
模型(Models)
Models 是从 Schema 编译来的构造函数,它们的实例就代表着可以从数据保存和读取 documents,从数据创建和读取 document 的所有操作都是通过 model 进行的。
const schema = new Schema({
name: 'string',
size: 'string'
});
const Tank = mongoose.model('Tank', schema);
第一个参数是跟 model 对应的集合(collection) 名字的单数形式。Mongoose 会自动找到名称是 model 名字复数形式的 collection。对于上例,Tank 这个model 就对应 数据库中 tanks 这个 collection. .model()
这个函数是对 schema
做了拷贝(生成了model)。要确保在调用 .model()
之前把所有需要的东西都加进去 schema
里面。
构造 documents
Documents 是 model 的实例,创建它们并保存到数据库非常简单:
const Tank = mongoose.model('Tank', yourSchema);
const small = new Tank({
size: 'small'
});
small.save(function (err) {
if (err) return handleError(err);
});
Tank.create(
{
size: 'small'
},
function (err, small) {
if (err) return handleError(err);
}
);
要注意,知道 model 使用的数据连接(connection)被打开,tanks 才会被创建/删除,每个 model 都有一个绑定的连接。如果model 是通过调用 mongoose.model()
生成的,它将使用 mongoose 的默认连接。
mongoose.connect('localhost', 'gettingstarted');
如果自行创建了连接,就需要使用 connection 的 model
函数代替 mongoose 的 model
函数
const connection = mongoose.createConnection('mongodb://localhost:27017/test');
const Tank = connection.model('Tank', yourSchema);
查询
用 mongoose 查询文档相当容易,它支持 mongoDB的高级查询语法(rich),查询文档可以使用 model
的 find, findById, findOne, where 这些静态方法。
Tank.find({
size: 'small'
})
.where('createDate')
.gt(oneYearAgo)
.exec(callback);
删除
model
的 remove
方法可以删除所有匹配查询条件的文档
Tank.remove(
{
size: 'large'
},
function (err) {
if (err) return handleError(err);
}
);
更新
model
的 update
方法可以修改数据库中的文档,不过不会把文档返回给应用层。
如果想更新单独一条文档并且返回给应用层,可以使用 findOneAndIUpdate 方法
文档(Documents)
Mongoose documents代表着MongoDB 文档的一对一映射,每个 document 都是他的 Model 实例。
检索
MongoDB 有很多检索数据的方法。
更新
Document 更新的方法同样也有很多, 最基本的 findById
Tank.findById(id, function (err, tank) {
if (err) return handleError(err);
tank.size = 'large';
tank.save(function (err, updateTank) {
if (err) return handleError(err);
res.send(updateTank);
});
});
也可以用 .set()
修改 document 。在底层, tank.size = 'large'
,用 tank.set({size:'large'})
Tank.findById(id, function (err, tank) {
if (err) return handleError(err);
tank.set({
size: 'large'
});
tank.save(function (err, updateTank) {
if (err) return handleError(err);
res.send(updateTank);
});
});
这个方法先检索了数据,接着更新(使用了 save
)。如果我们仅仅需要更新而不是获取该数据, model#update
就很适合我们:
Tank.update(
{
_id: id
},
{
$set: {
size: 'large'
}
},
callback
);
如果我们确实需要返回文档,下面的方法更加适合:
Tank.findByIdAndUpdate(
id,
{
$set: {
size: 'large'
}
},
{
new: true
},
function (err, tank) {
if (err) return handleError(err);
res.send(tank);
}
);
findAndUpdate/Remove
系列静态方法查找并返回最多1个文档,模式的方法有不少。
注意: findAndUpdata/Remove
不会修改数据库时执行任何钩子或者验证。可以使用 runValidators
选项获取一个验证的限制子集。但是需要钩子和全文档验证的,还是先query 在save
验证
Document 会在被保存之前验证。
覆盖
可以用 .set()
覆盖整个文档。如果要修改在中间件中被保存的文档,这样就很方便了。
Tank.findById(id, function (err, tank) {
if (err) return handleError();
// otherTank 是 tank 的副本
otherTank.set(tank);
});
子文档(Subdocuments)
子文档是指嵌套在另一个文档中的文档。在 Mongoose 中,这意味着可以在 里面嵌套一个 schema 。Mongoose 子文档有两种不同的概念:子文档数组和单个嵌套子文档。
const childSchema = new Schema({
name: 'string'
});
const parentSchema = new Schema({
children: [childSchema],
// Single nested subdocuments. Caveat: single nested subdocs only work in mongoose >= 4.2.0
child: childSchema
});
子文档和普通 docuemnt 类似。嵌套 schema 可以有自己的中间件,自定义检索逻辑、虚拟值已经其他顶层 schemas 可用的特性,两者主要的不同点是子文档不能单独保存,他们会在他们的顶级文档保存时保存。
const Parent = mongoose.model('Parent', parentSchema);
const parent = new Parent([children: [{
name: 'Matt'
}, {
name: 'Sarah'
}]]);
parent.children[0].name = 'HaHa';
// parent.children[0].save() 无操作,虽然他触发了中间件,但是没有保存文档,需要 save 他的文档、
parent.save(callback);
子文档跟普通文档一样有 save 和 validate 中间件,调用福文档的 save 会触发所有子文档的 save 中间件, validate 中间件同理。
childSchema.pre('save', function (next) {
if ('invalid' == this.name) {
return next(new Error('#sadpanda'));
}
next();
});
const parent = new Parent({
children: [
{
name: 'invalid'
}
]
});
parent.save(function (err) {
console.log(err.message); //#sadpanda
});
子文档的 pre('save')
和 pre('validate')
中间件执行于顶层 document pre('save')
之前,顶层 document 的 pre('validate')
之后。因为 save()
前的验证就是一个中间件。
const childSchema = new mongoose.Schema({
name: 'string'
});
childSchema.pre('validate', function (next) {
console.log(2);
next();
});
childSchema.pre('save', function (next) {
console.log(3);
next();
});
const parentSchema = new mongoose.Schema({
child: childSchema
});
parentSchema.pre('validate', function (next) {
console.log(1);
next();
});
parentSchema.pre('save', function (next) {
console.log(4);
next();
});
// 1 2 3 4
查找子文档
每个子文档都有一个默认 _id
,Mongoose document 数组有一个特别的 id 方法,这个方法只要传入 _id
就能返回文档数组中特定文档。
const doc = parent.children.id(_id);
添加子文档到数组
Mongoose 数组方法有 push、unshift、addToSet 等等:
const Parent = mongoose.model('Parent');
const parent = new Parent();
parent.children.push({
name: 'Haha'
});
const subdoc = parent.children[0];
subdoc; // _id: '501d86090d371bab2c0341c5', name: 'Haha' }
subdoc.isNew; // true
parent.save(function (err) {
if (err) return handleError(err);
console.log('Success');
});
create 方法可以新建文档但是不加入数组
const newdoc = parent.children.create({
name: 'Haha'
});
删除子文档
每个子文档都有 remove 方法,另外,对于子文档数组,有一个等效的方法 .pull()
。对于单个嵌套子文档,remove 与把这个文档的值设为 null 等效。
// 等效 parent.children.pull(_id)
parent.children.id(_id).remove();
// 或者 parent.child = null
parent.child.remove();
parent.save(function (err) {
if (err) return handleError(err);
console.log('the subdocs were removed');
});
代替声明语法的写法
如果用对象的数组创建 schema, mongoose 会自动把对象转成 schema:
const parentSchema = new Schema({
children: [
{
name: 'string'
}
]
});
// 等价于
const parentSchema = new Schema({
children: [
new Schema({
name: 'string'
})
]
});
查询(queries)
Model 的多个静态辅助方法都可以查询文档。
Model 的方法中包含查询条件参数的(find findById count update)都可以按照下面两种方式执行:
- 传入 callback 参数,操作会被立即执行,查询结果被传给回调函数(callback)
- 不传 callback 参数,Query 的一个实例(一个 query对象)被返回,这个 query 提供了 构建查询器的特殊接口。
Query 实例有一个 .then()
函数,用法类似 promise
如果执行查询时传入 callback, 就需要用 JSON 文档的格式指定查询条件,JSON 文档的语法跟 MongoDB shell 一致。
const Parent = mongoose.model('Person', mySchema);
// 查询 last name 是 haha 的 person,选中 name 和 occupation 字段
Person.findOne(
{
'name.last': 'haha'
},
'name occupation',
function (err, person) {
if (err) return handleError(err);
console.log(person.name.first, person.name.last, person.occupation);
}
);
上面的查询被立即执行,查询结果被传入回调函数。Mongoose 中所有的调用函数都是用 callback(error,result)
这种模式。如果查询发生错误, error
参数即是错误文档, result
参数会是 null。如果查询成功, error
参数是 null, result
即是查询结果。
查询结果的格式取决于做什么操作:findOne() 是单个文档(或者null),find() 是文档列表,count() 是文档数量, update() 是被修改的文档数量。
下面看看不传入 callback 这个参数会是怎么样的:
// 查找每个 last name 是 haha 的 person
const query = Person.findOne({
'name.last': 'haha'
});
// 选择 name 和 occupation 字段
query.select('name occupation');
// 然后执行查询
query.exec(function (err, person) {
if (err) return handleError(err);
console.log(person.name.first, person.name.last, person.occupation);
});
上面的代码中,query 是 Query 类型的变量,Query 能够用链式语法构建查询器,无需要执行 JSON 对象。下面2个实例等效:
Person.find({
occupation: /host/,
'name.last': 'haha',
age: {
$gt: 17,
$lt: 66
},
likes: {
$in: ['vaporizing', 'talking']
}
})
.limit(10)
.sort({
occupation: -1
})
.select({
name: 1,
ouccupation: 1
})
.exec(callback);
Person.find({
occupation: /host/
})
.where('name.last')
.equals('haha')
.where('age')
.gt(17)
.lt(66)
.where('likes')
.in(['vaporizing', 'talking'])
.limit(10)
.sort('-occupation')
.select('name occupation')
.exec(callback);
引用其他文档
MongoDB 中没有表连接,但引用其他结合的文档有时候也会需要。population 就是这样当升的。
Streaming
可以用 流式处理 MongoDB 的查询结果,需要调用 Query.cursor() 函数获得 QueryCursor 的一个实例:
const cursor = Person.find({
occupation: /host/
}).cursor();
cursor.on('data', function (doc) {
/**Called once for every document*/
});
cursor.on('close', function () {
/**Called when done*/
});
验证(validation)
需要先注意下面几点:
- '验证定义于 SchemaType'
- '验证是一个中间件,它默认作为
pre(save)
钩子注册在 schema 上' - '可以使用
doc.validate(callback)
或者doc.validateSync()
手动验证' - '验证器不对未定义的值进行验证,唯一例外就是 require 验证器'
- '验证是异步递归的。当调用 Model#save, 子文档也会执行,出错的话 Model#save 回调会接受错误'
- '验证是可以定制的。'
const schema = new Schema({
name: {
type: String,
require: true
}
});
const Cat = db.model('Cat', schema);
const cat = new Cat();
cat.save(function (err) {
assert.equal(error.errors['name'].message, 'Path `name` is required.');
error = cat.validateSync();
assert.equal(error.errors['name'].message, 'Path `name` is required.');
});
内建 Validators
Mongoose 有一些内建验证器
- '所有 SchemaTypes 都有内建的 required 验证器。required 验证器使用
checkRequired()
函数判定这个值是否满足 required 验证器' - 'Numbers 有 min 和 max 验证器'
- 'String 有 enum、match、maxlength 和 minlength 验证器'
上面的链接提供了使用和错误处理相关的详细信息。
const breakfastSchema = new Schema({
eggs: {
type: Number,
min: [6, 'Too few eggs'],
max: 12
},
bacon: {
type: Number,
required: [true, 'Why no bacon']
},
drink: {
type: String,
enum: ['Coffee', 'Tea'],
required: function () {
return this.bacon > 3;
}
}
});
const Breakfast = db.model('Breakfast', breakfastSchema);
const badBreakfast = new Breakfast({
eggs: 2,
bacon: 0,
drink: 'Milk'
});
const error = badBreakfast.validateSync();
assert.equal(error.errors['eggs'].message, 'Too few eggs');
assert.ok(!error.errors['bacon']);
assert.equal(error.errors['drink'].message, '`Milk` is not a valid enum value for path `drink`');
badBreakfast.bacon = 5;
badBreakfast.drink = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['drink'].message, 'Path `drink` is required');
badBreakfast.bacon = null;
error = badBreakfast.validateSync();
assert.equal(error.errors['bacon'].message, 'Why no bacon');
** unique 不是验证器**
它是构建 MongoDB unique indexes 的辅助函数
const uniqueUsernameSchema = new Schema({
username: {
type: String,
unique: true
}
});
const U1 = db.model('U1', uniqueUsernameSchema);
const U2 = db.model('U2', uniqueUsernameSchema);
const dup = [
{
username: 'Val'
},
{
username: 'val'
}
];
U1.create(dup, function (err) {});
// 在保存之前需要等待索引完成构建,否则可能会违反惟一的约束。
U2.once('index', function (err) {
assert.ifError(err);
U2.create(dup, function (err) {
// 重复键错误
assert.ok(err);
assert.ok(!err.errors);
assert.ok(error.message.indexOf('duplicate key error') != -1);
});
});
U2.init().then(function () {
U2.create(dup, function (err) {
// 重复键错误
assert.ok(err);
assert.ok(!err.errors);
assert.ok(error.message.indexOf('duplicate key error') != -1);
});
});
自定义验证器
如果内建检验器不够用了,可以定义满足自己需要的验证器
自定义验证器通过传入一个验证函数来定义
const userSchema = new Schema({
phone: {
type: String,
validate: {
validator: function (v) {
return /\d{3}-d{3}-d{4}/.test(v);
},
message: '{VALUE} is not a valid phone number!'
},
required: [true, 'User phone number required']
}
});
const User = db.model('user', userSchema);
const user = new User();
let error;
user.phone = '555.0123';
error = user.validateSync();
assert.equal(error.errors['phone'].message, '555.0123 is not a valid phone number!');
user.phone = '';
rror = user.validateSync();
assert.equal(error.errors['phone'].message, 'User phonse number required!');
user.phone = '201-555-0123';
rror = user.validateSync();
assert.equal(error, null);
异步自定义验证器
自定义验证器可以是异步的,如果检验函数返回 promise (像 async
函数),mongoose 将会等待该 promise 完成。如果更喜欢使用回调函数,设置 isAsync
选项,mongoose 会将回调函数作为验证函数的第二个参数。
const userSchema = new Schema({
name: {
type: String,
// 可以通过返回一个promise使验证器异步。如果返回一个promise,不要指定' isAsync '选项。
validate: function (v) {
return new Promise(function (resolve, reject) {
setTimeout(function () {
resolve(false);
}, 5);
});
}
},
phone: {
type: String,
validate: {
isAsync: true,
validator: function (v, cb) {
setTimeout(function () {
const phoneRegex = /\d{3}-d{3}-d{4}/;
const msg = v + 'is not a valid phone number!';
// 第一个参数是布尔值代表验证结果
// 第二个参数是报错信息
cb(phoneRegex.test(v), msg);
}, 5);
},
// 默认报错信息会被 cb 的第二个参数覆盖
message: 'Default error message'
},
required: [true, 'User phone number required']
}
});
const User = db.model('User', userSchema);
const user = new User();
let error;
user.phone = '555.0123';
user.name = 'test';
user.validate(function (error) {
assert.ok(error);
assert.equal(error.errors['phone'].message, '555.0123 is not a valid phone number!');
assert.equal(error.errors['name'].message, 'Validator failed for path `name` with value `test`');
});
验证错误
验证失败返回 的err 包含一个 ValidatorError
对象。每一个 ValidatorError 都有 kind、path、value 和 message 属性。ValidatorError 也有可能有 reason 属性如果验证器抛出错误,这个属性会包含该错误原因。
const toySchema = new Schema({
color: String,
name: String
});
const validator = function(v) {
return /red|white|gold/i.test(v);
}
toySchema.path('color').validate(validator, 'Color `{VALUE}` not valid', 'Invalid color'); toySchema.path('name').validate(function(v) {
if (v !== 'Turbo Man') {
throw new Error('Need to get a Turbo Man for Christmas');
}
return ture;
}, 'Name `{VALUE}` is not valid');
const Toy = db.model('Toy', toySchema);
const toy = new Toy({
color: 'Green',
name: 'Power Ranger'
}); toy.save(function(err) {
assert.equal(err.errors.color.message, 'Color `Green` not valid');
assert.equal(err.errors.color.kind, 'Invalid color');
assert.equal(err.errors.color.path, 'color');
assert.equal(err.errors.color.value, 'Green');
assert.equal(err.errors.name.message, 'Need to get a Turbo Man for Christmas');
assert.equal(err.errors.name.value, 'Power Ranger');
assert.equal(err.errors.name.reason.message, 'Need to get a Turbo Man for Christmas');
assert.equal(err.name, 'ValidationError');
});
嵌套对象中的 Required 验证器
定义嵌套对象的验证器需要特别注意
let personSchema = new Schema({
name: {
first: String,
last: String
}
});
assert.throws(function () {
// 这里会报错,因为 name 不是完整路径
personSchema.path('name').required(true);
});
// 要让嵌套对象 required 要使用单独的嵌套 schema
const nameSchema = new Schema({
name: {
type: nameSchema,
required: true
}
});
personSchema = new Schema({
name: {
type: nameSchema,
required: true
}
});
const person = db.model('Person', personSchema);
const person = new Person();
const error = person.validateSync();
assert.ok(error.errors['name']);
Update 验证器
Mongoose 还支持验证 update() 和 findOneAndUpdate() 操作,Update 验证器默认关闭,如果需要打开,需要配置 runValidators
const toySchema = new Schema({
color: String,
name: String
});
const Toy = db.model('Toys', toySchema);
Toy.schema.path('color').validate(function (value) {
return /blue|green|white|red|orange|periwinkle/i.test(value);
}, 'Invalid color');
const opts = {
runValidators: true
};
Toy.update(
{},
{
color: 'bacon'
},
opts,
function (err) {
assert.equal(err.errors.color.message, 'Invalid color');
}
);
**Update 验证器 与 this **
update 验证器和 document 验证器有很多不同。上面的颜色验证函数,this 指向验证中的 document,然后update 验证器运行时,被更新文档不一定存在于服务器内存,所以 this 值未定义
const toySchema = new Schema({
color: String,
name: String
});
toySchema.path('color').validate(function (value) {
// 在运行“validate()”或“validateSync()”时
// 验证器可以使用“this”访问文档。
if (this.name.toLowerCase().indexOf('red') !== -1) {
return value !== 'red';
}
return true;
});
const Toy = db.model('ActionFigure', toySchema);
const toy = new Toy({
color: 'red',
name: 'Red Power Ranger'
});
const error = toy.validateSync();
assert.ok(error.errors['color']);
const update = {
color: 'red',
name: 'Red Power Ranger'
};
const opts = {
runValidators: true
};
Toy.update({}, update, opts, function (error) {
// The update validator throws an error:
// "TypeError: Cannot read property 'toLowerCase' of undefined",
// because `this` is **not** the document being updated when using
// update validators
assert.ok(error);
});
** context 选项**
context 选项运行把 update 验证器的 this 设定为 query
toySchema.path('color').validate(function (v) {
// When running update validators with the `context` option set to
// 'query', `this` refers to the query object.
if (this.getUpdate().$set.name.toLowerCase().indexOf('red') !== -1) {
return value == 'red';
}
return true;
});
const Toy = db.model('Figure', toySchema);
const update = {
color: 'blue',
name: 'haha'
};
// Note the context option
const opts = {
runValidators: true,
context: 'query'
};
Toy.update({}, update, opts, function (error) {
assert.ok(error.errors['color']);
});
Update 验证器字段路径
另一个关键不同点是 update 验证器只运行于更新的字段。 下例中,因为 'name' 在更新操作未被指定,所以此次更新操作成功。
使用 update 验证器的时候, required
验证器只会在你对某个字段显式使用 $unset
才会触发。
const kittenSchema = new Schema({
name: {
type: String,
required: true
},
age: Number
});
const Kitten = db.model('Kitten', kittenSchema);
const update = {
color: 'blue'
};
const opts = {
runValidators: true
};
Kitten.update({}, update, opts, function (err) {
// 即使 'name' 没有指定也操作成功了
});
const unset = {
$unset: {
name: 1
}
};
Kitten.update({}, unset, opts, function (err) {
// 'name' required, 操作失败
assert.ok(err);
assert.ok(err.errors['name']);
});
Update 验证器只运行于指定字段路径
最后要注意的是:update 验证器只运行于下列更新操作:
- '
$set
' - '
$unset
' - '
$push
(>= 4.8.0)' - '
$addToSet
(>= 4.8.0)' - '
$pull
(>= 4.12.0)' - '
$pullAll
(>= 4.12.0)'
例如,以下 update 成功执行,不管 number
的值,因为 update 验证器 无视 $inc
。同样, $push
、 $addToSet
、 $pull
和 $pullAll
验证器 不会对数组自身验证,只会对数组中的元素验证。
const testSchema = new Schema({
number: {
type: Number,
max: 0
},
arr: [
{
message: {
type: String,
maxlength: 10
}
}
]
});
// Update 验证器不会作检查,所以你再仍然可以 `$push` 两个元素到数组
// 只要他们的 `message` 没有超长
testSchema.path('arr').validate(function (v) {
return v.length < 2;
});
const Test = db.model('Test', testSchema);
const update = {
$inc: {
number: 1
}
};
const opts = {
runValidators: true
};
Test.update({}, update, opts, function (error) {
// 这里不会报错
update = {
$push: [
{
message: 'hello'
},
{
message: 'world'
}
]
};
Test.update({}, update, opts, function (error) {
// 这里也不会报错
});
});
$push 和 $addToSet
4.8.0 新特性: update 验证器也运行于 $push
和 $addToSet
const testSchema = new Schema({
numbers: [
{
type: Number,
max: 0
}
],
docs: [
{
name: {
type: String,
required: true
}
}
]
});
const Test = db.model('TestPush', testSchema);
const update = {
$push: {
numbers: 1,
docs: {
name: null
}
}
};
const opts = {
runValidators: true
};
Test.update({}, update, opts, function (error) {
assert.ok(error.errors['numbers']);
assert.ok(error.errors['docs']);
});
中间件(middleware)
中间件(pre 和 post)是在异步函数执行时函数传入的控制函数。中间件在 Schema 上指定,在写插件时很有用。Mongoose 4.x 有四种中间件:document、model、aggregate、query 中间件。对于 document 中间件,this 指向当前的 document,Document 中间件支持下面的 document 操作:
- 'init'
- 'validate'
- 'save'
- 'remove'
对于 query中间件,this 指向 query。Query 中间件支持一下 Model 和 Query 操作:
- 'count'
- 'find'
- 'findOne'
- 'findOneAndRemove'
- 'findOneAndUpdate'
- 'update'
Aggregate 中间件作用于 MyModel.aggregat(), 它会在对 aggregate 对象调用 exec() 时执行。对于 aggregate 中间件,this 也是指向 aggregation 对象。
- 'aggregate'
对于 model 中间件来说,this 是指向当前的model. Model 中间件支持下面的操作:
- 'insertMany'
所有的中间件支持 pre 和 post 钩子,下面解释这两个钩子的细节。
注意:Query 是没有 remove 钩子的,只有 document有,如果设定了 remove 钩子,将会在调用 myDoc.remove()而不是MyModel.remove 的时候触发。只有 create() 函数会触发 save() 钩子。
Pre
pre 钩子分为串行和并行两种。
串行
串行中间件一个接一个地执行,具体来说,上一个中间件调用 next 函数的时候,下一个执行。
const schema = new Schema({
..
});
schema.pre('save', function(next) {
next();
});
在 5.x 版本中,除了手动调用 next ,可以返回一个 promise ,甚至是 async/await。
schema.pre('save', function () {
return doStuff().then(() => doMoreStuff());
});
// 或者 Node.js >= 7.6.0:
schema.pre('save', async function () {
await doStuff();
await doMoreStuff();
});
next() 不会阻止剩余代码的运行,可以使用提前 return 模式来阻止 next () 后面的代码。
const schema = new Schema({
..
});
schema.pre('save', function(next) {
if (foo()) {
console.log('calling next');
// `return next();` will make sure the rest of this function doesn't run
next();
}
// Unless you comment out the `return` above, 'after next' will print
console.log('after next');
});
并行
并行中间件提供细粒度流控制
const schema = new Schema({
..
});
// true 代表这是一个并行中间件,如果要使用并行中间件,必须指定 true 为第二个参数
schema.pre('save', true, function(next, done) {
next();
setTimeout(done, 100);
});
上述的例子中,save 方法将在所有中间件都调用了 done 的时候才会执行。
使用场景:
中间件对原子化模型逻辑很有帮助,这里有一些其他建议:
- '复杂的数据校验'
- '删除依赖文档(删除用户后删除其他所有文章)'
- '某个操作触发的异步任务'
错误处理:
如果 pre 钩子出错,momgoose 将不会执行后面的函数。Mongoose 会向回调函数传入 err 参数,或者 reject 返回的 promise 。下面是几个错误的处理的方法:
schema.pre('save', function (next) {
const err = new Error('something went gone');
next(err);
});
schema.pre('save', function (next) {
return new Promise((resolve, reject) => {
reject(new Error('something went gone'));
});
});
schema.pre('save', async function () {
await Promise.resolve();
throw new Error('something went gone');
});
myDoc.save(function (err) {
console.log(err.message);
});
多次调用 next 是无效的,如果调用 next 带有错误参数 err1 ,然后再抛出一个 err2,mongoose 只会传递 err1
Post中间件
post 中间件在方法执行之后调用,这个时候每个 pre 中间件都已经完成了。
schema.post('init', function (doc) {
console.log('%s has been initialized from the db', doc._id);
});
schema.post('validate', function (doc) {
console.log('%s has been validated (but not saved yet)', doc._id);
});
schema.post('save', function (doc) {
console.log('%s has been saved', doc._id);
});
schema.post('remove', function (doc) {
console.log('%s has been removed', doc._id);
});
异步 Post 钩子
如果给回调函数传入两个参数,mongoose 会认为第二个参数 next 函数,可以通过 next 触发下一个中间件
schema.post('save', function (doc, next) {
setTimeout(function () {
console.log('post1');
next();
}, 10);
});
// 直到第一个中间件执行 next() 才会执行
schema.post('save', function (doc, next) {
console.log('post2');
next();
});
Save/Validate钩子
save() 函数触发 validate 钩子,mongoose validate() 其实就是 pre('save')钩子,这意味着所有 pre('validate') 和 post('validate')都会在 pre('save')钩子之前调用。
schema.pre('validate', function () {
console.log('this gets printed first');
});
schema.post('validate', function () {
console.log('this gets printed second');
});
schema.pre('save', function () {
console.log('this gets printed third');
});
schema.post('save', function () {
console.log('this gets printed fourth');
});
finAndUpdate() 与 Query 中间件使用注意
pre 和 post save() 钩子都不执行于 update , findOneAndUpdate 等情况。Mongoose4.0 为这些函数制定了新钩子。
schema.pre('find', function() {
console.log(this instanceof mongoose.Query); // true
this.start = Date.now()
});
schema.post('find', function(result) {
console.log(this instanceof mongoose.Query); // true
// prints returned documents
console.log('find() returned ' + JSON.stringify(result));
// prints number of milliseconds the query took
console.log('find() took ' + (Date.now() - 'this.start) + '
millis ');'
});
Query 中间件不同于 document 中间件,document 中间件中,this 指向被更新 document, query 中间件中,this 指向query 对象而不是被更新 document
例如,如果要每次 update 之前更新 updateAt 时间戳,可以使用 pre 钩子
schema.pre('update', function () {
this.update(
{},
{
$set: {
updateAt: new Date()
}
}
);
});
错误处理中间件
错误处理中间件比普通中间件多一个 error
参数,并且 err
作为第一个参数传入。 而后错误处理中间件可以让你自由地做错误的后续处理。
const schema = new Schema({
name: {
type: String,
// // Will trigger a MongoError with code 11000 when you save a duplicate
unique: true
}
});
// 处理函数必须传入3个参数:发生的错误,返回文件,以及 next 函数
schema.post('save', function (error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(error);
}
});
// Will trigger the `post('save')` error handler
Person.create([
{
name: 'Axl Rose'
},
{
name: 'Axl Rose'
}
]);
对于query 中间件也可以使用错误处理,可以定义一个 post update 钩子,它可以捕获 MongoDB 重复 key 错误。
schema.post('update', function (error, res, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(error);
}
});
const people = [
{
name: 'Axl Rose'
},
{
name: 'Axl Rose'
}
];
Person.create(people, function (error) {
Person.update(
{
name: 'haha'
},
{
$set: {
name: 'Axl Rose'
}
},
function (error) {
// error.message` will be "There was a duplicate key error"
}
);
});
填充(Populate)
像sql 里面的 join 的聚合操作,populate 可以让你在别的 collection 中引用 document
Population 可以自动替换 document 中的指定字段,替换内容从其他 collection 获取。我们可以填充单个或者每个 document、单个或者多个纯对象,甚至是 query 返回的一切对象。
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const personSchema = Schema({
_id: Schema.Types.ObjectId,
name: String,
age: Number,
stories: [
{
type: Schema.Types.ObjectId,
ref: 'Story'
}
]
});
const storySchema = Schema({
author: {
type: Schema.Types.ObjectId,
ref: 'Person'
},
title: String,
fans: [
{
type: Schema.Types.ObjectId,
ref: 'Person'
}
]
});
const Story = mongoose.model('Story', storySchema);
const Person = mongoose.model('Person', personSchema);
上面创建了两个 Model,Person model 的 stories 字段设为 ObjectId 数组。ref 选项告诉 Mongoose 在填充的时候使用哪个 model,上例中为 Story 的 model. 所有存储在此的 _id 必须是 Story model 中的 document 的 _id。
注意:ObjectId、Number、String 以及 Buffer 都可以作为 refs 使用。但是最好还是用 ObjectId
保存 refs
保存 refs 与保存普通属性一样,把 _id 的值赋给它就好了:
const author = new Person({
_id: new mongoose.Types.ObjectId(),
name: 'Ian Fleming',
age: 50
});
author.save(function (err) {
if (err) return handleError(err);
var story1 = new Story({
title: 'Casino Royale',
author: author._id // assign the _id from the person
});
story1.save(function (err) {
if (err) return handleError(err);
// thats it!
});
});
Population
我们做的东西还是跟平时差不多,只是创建了 Person
和 Story
。现在试试对query 填充 story 的 author。
Story.findOne({
title: 'haha'
})
.population('author')
.exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name);
});
被填充的字段已经不再是原来的 _id,而是被指定的 document 替代,这个 document 由另一条 query 从数据库返回。refs 数组的原理也是相似的。对 query 对象调用 populate 方法,就能返回装载对应 _id 的 document 数组。
设置被填充字段
Story.findOne(
{
title: 'Casino Royale'
},
function (error, story) {
if (error) {
return handleError(error);
}
story.author = author;
console.log(story.author.name); // prints "Ian Fleming"
}
);
字段选择
如果我们只需要填充 document 其中一部分字段,第二个参数传入 field name syntax 就可以实现。
Story.findOne({
title: /casino royale/i
})
.populate('author', 'name') // only return the Persons name
.exec(function (err, story) {
if (err) return handleError(err);
console.log('The author is %s', story.author.name); // The author is Ian Fleming
console.log('The author is %s', story.author.age); // The authors age is null
});
填充多个字段
Story.
find(...).
populate('fans').
populate('author').
exec();
如果对同一路径 populate 两次,只有最后一次生效。
// 第二个会覆盖第一个的
Story.find()
.populate({
path: 'fans',
select: 'name'
})
.populate({
path: 'fans',
select: 'email'
});
// The above is equivalent to:
Story.find().populate({
path: 'fans',
select: 'email'
});
Query 条件与其他选项
如果要根据年龄来填充,只填充 name, 并且值返回最多5个数据
Story.
find(...).
populate({
path: 'fans',
match: {
age: {
$gte: 21
}
},
// Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
select: 'name -_id',
options: {
limit: 5
}
}).
exec();
Refs 到 children
我们发现,用 author 对象没有办法获取 story 列表,因为 author.stories 没有被 pushed 任何 story 对象
我们希望 author 知道哪些 story 属于他们,通常,schema 应该通过在 多 的一方使用指向它们的父节点(parent pointer)解决一对多关系问题。另一方面,如果有充分理由得到指向子节点的数组,可以像下面代码一样把 document push() 到数组中。
author.stories.push(story1);
author.save(callback);
然后我们就能 find 和 populate 了
Person.findOne({
name: 'Ian Fleming'
})
.populate('stories')
.exec(function (err, person) {
if (err) throw handleError(err);
console.log(person);
});
如果父子节点互相指向,数据可能会在某一时刻失去同步。为此我们可以不使用填充,直接 find()
我们需要的 story
Story.find({
author: author._id
}).exec(function (err, stories) {
if (err) return handleError(err);
console.log('The stories are an array: ', stories);
});
query 填充后返回的 document 功能齐全,除非设置 lean 选项,否则它就是可 remove,可 save 的。调用 remove 不单是从数组中删除也是从数据库中删除它们。
填充现有 document
document#populate()
填充多个现有 document
如果需要填充一个或者多个 document 对象,我们可以使用 Model.populate 方法。
多级填充
假设 user schema 记录了 user 的 friends
const userSchema = new Schema({
name: String,
friends: [
{
type: ObjectId,
ref: 'User'
}
]
});
当然可以填充得到用户的 friends 列表,但是如果要在获得他们朋友的朋友呢?指定 populate
选项就可以了
User.findOne({
name: 'Val'
}).populate({
path: 'friends',
populate: {
path: 'friends'
}
});
跨数据库填充
假设现在有 event schema 和 conversation schema, 每个 event 对应一个 conversation 线程
const eventSchema = new Schema({
name: String,
conversation: ObjectId
});
const conversationSchema = new Schema({
numMessage: Number
});
并且,event 和 conversation 保存在不同的 MongoDB 实例中
const db1 = mongoose.createConnection('localhost:27000/db1');
const db2 = mongoose.createConnection('localhost:27001/db2');
const Event = db1.model('Event', eventSchema);
const Conversation = db2.model('Conversation', conversationSchema);
这个情况就不能直接使用 populate()
了,因为 populate()
不知道应该使用什么填充。 可以显式指定一个 model
Event.find()
.population({
path: 'conversation',
model: Conversation
})
.exec(function (err, docs) {});
动态引用
Mongoose 也可以从多个 collection 填充。假设 user schema 有一系列 connection,一个 user 可以连接到其他 user 或者组织。
const userSchema = new Schema({
name: String,
connection: [
{
kind: String,
item: {
type: ObjectId,
refPath: 'connections.kind'
}
}
]
});
const organizationSchema = new Schema({
name: String,
kind: String
});
const User = mongoose.model('User,userSchema');
const Organization = mongoose.model('Organization', organizationSchema);
上面的 refPath
属性意味着 mongoose 会查找 connections.kind
路径, 以此确定 populate()
使用的 model。换句话说, refPath
属性可以让你动态寻找 ref
。
鉴别器(Discriminators)
Discriminator 是一种 schema 继承机制。它允许在相同的底层 MongoDB collection 上使用部分重叠的 schema 建立多个 model。
假设要在单个 collection 中记录多种 event ,每个 event 都有时间戳字段,但是 click 事件还有 URL 字段,可以用 model.discriminator 实现上面的要求。这个函数需要两个参数,model 名称和 discriminator schema, 返回 model 结合原 model 的 schema 和 discriminator schema.
const options = {
discriminatorKey: 'kind'
};
const eventSchema = new mongoose.Schema(
{
tiem: Date
},
options
);
const Event = mongoose.model('Event'.eventSchema);
// ClickedLinkEvent 是一个有 URL 的特别 event
const ClickedLinkEvent = Event.discriminator(
'ClickedLink',
new mongoose.Schema(
{
url: String
},
options
)
);
// 当你创建通用 event,他将没有 URL 字段...
const genericEvent = new Event({
time: Date.now(),
url: 'google.com'
});
assert.ok(!genericEvent.url);
// 但是 ClickedLinkEvent 可以有
const clickedEvent = new ClickedLinkEvent({
time: Date.now(),
url: 'google.com'
});
assert.ok(clickedEvent.url);
Discriminator 储存在 Event model 的 collection
现在假设你要创建另一个 discriminator,记录用户注册 event。 SignedUpEvent
实例将跟 通用 events 和 ClickedLinkEvent
实例 一样储存在同一个 collection。
const event1 = new Event({
time: Date.now()
});
const event2 = new ClickedLinkEvent({
time: Date.now(),
url: 'google.com'
});
const event3 = new SignedUpEvent({
time: Date.now(),
user: 'testuser'
});
const save = function (doc, callback) {
doc.save(function (error, doc) {
callback(error, doc);
});
};
async.map([event1, event2, event3], save, function (error) {
Event.count({}, function (error, count) {
assert.equal(count, 3);
});
});
Discriminator keys
Mongoose 通过 'discriminator key' 识别两个不同的 discriminator, 这个值默认是 __t
。Mongoose 自动在你的 schema 添加 __t
字段, 记录你的 document 是哪个 discriminator 的实例。
const event1 = new Event({
time: Date.now()
});
const event2 = new ClickedLinkEvent({
time: Date.now(),
url: 'google.com'
});
const event3 = new SignedUpEvent({
time: Date.now(),
user: 'testuser'
});
assert.ok(!event1.__t);
assert.equal(event2.__t, 'ClickedLink');
assert.equal(event3.__t, 'SignedUp');
Discriminator 在查询中添加 discriminator key
Discriminator model 的特别之处在于:他们会把 discriminator key 附到 query 上。换句话说, find()
, count()
, aggregate()
等方法 都能适配 discriminators。
const event1 = new Event({
time: Date.now()
});
const event2 = new ClickedLinkEvent({
time: Date.now(),
url: 'google.com'
});
const event3 = new SignedUpEvent({
time: Date.now(),
user: 'testuser'
});
const save = function (doc, callback) {
doc.save(function (error, doc) {
callback(error, doc);
});
};
async.map([event1, event2, event3], save, function (error) {
ClickedLinkEvent.find({}, function (error, docs) {
assert.equal(docs.length, 1);
assert.equal(docs[0]._id.toString(), event2._id.toString());
assert.equal(docs[0].url, 'google.com');
});
});
Discriminator 复制 pre / post 钩子
Discriminator 会继承他的基础 schema 的 pre 和 post 中间件。 不过,你也可以为 discriminator 添加中间件,这不回影响到基础 schema。
const options = {
discriminatorKey: 'kind'
};
const eventSchema = new mongoose.Schema(
{
time: Date
},
options
);
const eventSchemaCalls = 0;
eventSchema.pre('validate', function (next) {
++eventSchemaCalls;
next();
});
const Event = mongoose.model('GenericEvent', eventSchema);
const clickedLinkSchema = new mongoose.Schema(
{
url: String
},
options
);
const clickedSchemaCalls = 0;
clickedLinkSchema.pre('validate', function (next) {
++clickedSchemaCalls;
next();
});
const ClickedLinkEvent = Event.discriminator('ClickedLinkEvent', clickedLinkSchema);
const event1 = new ClickedLinkEvent();
event1.validate(function () {
assert.equal(eventSchemaCalls, 1);
assert.equal(clickedSchemaCalls, 1);
const generic = new Event();
generic.validate(function () {
assert.equal(eventSchemaCalls, 2);
assert.equal(clickedSchemaCalls, 1);
});
});
处理自定义 _id 字段
Discriminator 的字段是基础 schema 加 discriminator schema , 并且以 discriminator schema 的字段优先。 但有一个例外, _id
字段。
You can work around this by setting the _id
option to false in the discriminator schema as shown below.
const options = {
discriminatorKey: 'kind'
};
// 基础 schema 有字符串格式的 `_id` 字段和 Data 格式的 `time` 字段...
const eventSchema = new mongoose.Schema(
{
_id: String,
time: Date
},
options
);
const Event = mongoose.model('BaseEvent', eventSchema);
const clickedLinkSchema = new mongoose.Schema(
{
url: String,
time: String
},
options
);
// 但是 Discriminator schema 有字符串格式的 `time`,并且有
// 隐式添加的 ObjectId 格式的 `_id`
assert.ok(clickedLinkSchema.path('_id'));
assert.equal(clickedLinkSchema.path('_id').instance, 'ObjectID');
const ClickedLinkEvent = Event.discriminator('ChildEventBad', clickedLinkSchema);
const event1 = new ClickedLinkEvent({
_id: 'custom id',
time: '4pm'
});
// 问题来了,clickedLinkSchema 重写了 `time` 路径,但是**没有**
// 重写 `_id` 路径,因为已经隐式添加(没看懂)
assert.ok(typeof event1._id === 'string');
assert.ok(typeof event1.time === 'string');
**discriminator 与 Model.create() **
当你使用 Model.create()
,Mongoose 会自动帮你适配 discriminator key ~
const Schema = mongoose.Schema;
const shapeSchema = new Schema(
{
name: String
},
{
discriminatorKey: 'kind'
}
);
const Shape = db.model('Shape', shapeSchema);
const Circle = Shape.discriminator(
'Circle',
new Schema({
radius: Number
})
);
const Square = Shape.discriminator(
'Square',
new Schema({
side: Number
})
);
const shapes = [
{
name: 'Test'
},
{
kind: 'Circle',
radius: 5
},
{
kind: 'Square',
side: 10
}
];
Shape.create(shapes, function (error, shapes) {
assert.ifError(error);
// 重点看这里
assert.ok(shapes[0] instanceof Shape);
assert.ok(shapes[1] instanceof Circle);
assert.equal(shapes[1].radius, 5);
assert.ok(shapes[2] instanceof Square);
assert.equal(shapes[2].side, 10);
});
数组中的嵌套 discriminator
你也可以为嵌套文档数组定义 discriminator。 嵌套 discriminator 的特点是:不同 discriminator 类型储存在相同的文档而不是同一个 mongoDB collection。 换句话说,嵌套 discriminator 让你 在同一个数组储存符合不同 schema 的子文档。
最佳实践:确保你声明了钩子再使用他们。 你不应当在调用 discriminator()
之后调用 pre()
或 post()
const eventSchema = new Schema(
{
message: String
},
{
discriminatorKey: 'kind',
_id: false
}
);
const batchSchema = new Schema({
events: [eventSchema]
});
// `batchSchema.path('events')` gets the mongoose `DocumentArray`
const docArray = batchSchema.path('events');
// 这个 `events` 数组可以包含 2 种不同的 event 类型,
// 'clicked' event that requires an element id that was clicked...
const clickedSchema = new Schema(
{
element: {
type: String,
required: true
}
},
{
_id: false
}
);
// 确定在调用 `discriminator()` **之前**
// 对 `eventSchema` 和 `clickedSchema` 赋予钩子
const Clicked = docArray.discriminator('Clicked', clickedSchema);
// ... and a 'purchased' event that requires the product that was purchased.
const Purchased = docArray.discriminator(
'Purchased',
new Schema(
{
product: {
type: String,
required: true
}
},
{
_id: false
}
)
);
const Batch = db.model('EventBatch', batchSchema);
// Create a new batch of events with different kinds
const batch = {
events: [
{
kind: 'Clicked',
element: '#hero',
message: 'hello'
},
{
kind: 'Purchased',
product: 'action-figure-1',
message: 'world'
}
]
};
Batch.create(batch)
.then(function (doc) {
assert.equal(doc.events.length, 2);
assert.equal(doc.events[0].element, '#hero');
assert.equal(doc.events[0].message, 'hello');
assert.ok(doc.events[0] instanceof Clicked);
assert.equal(doc.events[1].product, 'action-figure-1');
assert.equal(doc.events[1].message, 'world');
assert.ok(doc.events[1] instanceof Purchased);
doc.events.push({
kind: 'Purchased',
product: 'action-figure-2'
});
return doc.save();
})
.then(function (doc) {
assert.equal(doc.events.length, 3);
assert.equal(doc.events[2].product, 'action-figure-2');
assert.ok(doc.events[2] instanceof Purchased);
done();
})
.catch(done);
检索数组中的嵌套 discriminator
检索嵌套 discriminator
const singleEventSchema = new Schema(
{
message: String
},
{
discriminatorKey: 'kind',
_id: false
}
);
const eventListSchema = new Schema({
events: [singleEventSchema]
});
const subEventSchema = new Schema(
{
sub_events: [singleEventSchema]
},
{
_id: false
}
);
const SubEvent = subEventSchema.path('sub_events').discriminator('SubEvent', subEventSchema);
eventListSchema.path('events').discriminator('SubEvent', subEventSchema);
const Eventlist = db.model('EventList', eventListSchema);
// Create a new batch of events with different kinds
const list = {
events: [
{
kind: 'SubEvent',
sub_events: [
{
kind: 'SubEvent',
sub_events: [],
message: 'test1'
}
],
message: 'hello'
},
{
kind: 'SubEvent',
sub_events: [
{
kind: 'SubEvent',
sub_events: [
{
kind: 'SubEvent',
sub_events: [],
message: 'test3'
}
],
message: 'test2'
}
],
message: 'world'
}
]
};
Eventlist.create(list)
.then(function (doc) {
assert.equal(doc.events.length, 2);
assert.equal(doc.events[0].sub_events[0].message, 'test1');
assert.equal(doc.events[0].message, 'hello');
assert.ok(doc.events[0].sub_events[0] instanceof SubEvent);
assert.equal(doc.events[1].sub_events[0].sub_events[0].message, 'test3');
assert.equal(doc.events[1].message, 'world');
assert.ok(doc.events[1].sub_events[0].sub_events[0] instanceof SubEvent);
doc.events.push({
kind: 'SubEvent',
sub_events: [
{
kind: 'SubEvent',
sub_events: [],
message: 'test4'
}
],
message: 'pushed'
});
return doc.save();
})
.then(function (doc) {
assert.equal(doc.events.length, 3);
assert.equal(doc.events[2].message, 'pushed');
assert.ok(doc.events[2].sub_events[0] instanceof SubEvent);
done();
})
.catch(done);