Mongoose

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的。它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。Mongo最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

Mongoose是在node.js环境下对mongodb进行便捷操作的对象模型工具

学习链接

快速上手

请先安装 MongoDB 和 Node.js。

下一步,npm安装Mongoose:

1
npm i mongoose

连接数据库test

1
2
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');

connect()返回一个状态待定(pending)的连接,接着我们加上成功和失败的提醒:

1
2
3
4
5
const db = mongoose.connection;
db.on('error',console.error.bind(console,'connection error:'));
db.once('open',function(){
// ..
});

连接成功的时候,回调函数会被调用,假设下面所有函数都在这个回调函数里面。

Mongoose 里面一切始于 Schema。看一个简单的例子:

1
2
3
const KittySchema = mongoose.Schema({
name:String
});

我们得到了一个带有 String类型的 name 属性的 schema,接着把这个 schema 编译成一个 Model:

1
const Kitten = mongoose.model('Kitten',kittySchema);

model 是我们创造的 document 的 Class。在例子中,每个 document 都是一个实例,它的属性和行为都会被声明在 schema 。

1
2
3
4
5
6
7
8
9
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 实例:

1
2
const a = new Kitten({name:'A'});
a.speak(); // Meow name is A

每个 document 会在调用它的 save 方法后保存到数据库中,注意回调函数的第一个参数永远是 error

1
2
3
4
A.save(function(err,A){
if(err) return console.error(err);
A.speak();
});

如果有很多个实例,可以用下面的方法来获取 model 里面的所有数据

1
2
3
4
Kitten.find(function(err,kittens){
if(err) return console.error(err);
console.log(kittens);
})

模式(Schemas)

定义一个schema

Mongoose 的一切始于 Schema,每个 schama 都会映射到一个 MongoDB collection,并定义这个 collection 里面的文档的构成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:

1
const Blog = mongoose.model('Blob',blogSchema);

实例方法(method)

document 是 Models 的实例。Document 有很多自带的实例方法,当然也可以自定义我们自己的方法

1
2
3
4
5
6
// 定义一个 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 方法:

1
2
3
4
5
6
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 举例:

1
2
3
4
5
6
7
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 实例,方便自定义扩展链式查询

1
2
3
4
5
6
7
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 级别定义。

1
2
3
4
5
6
7
8
9
10
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’ 事件。在生产中建议禁止这种行为,因为创建索引会对性能造成重大影响。

1
2
3
4
5
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 事件

1
2
3
4
5
6
7
// 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 可以很方便地分解一个值到多个字段。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 属性,但不必保存到数据库。

1
2
3
4
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,下面的例子中,当赋值到虚拟值的时候,会自动拆分到其他属性

1
2
3
4
5
6
7
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 会无缝链接到另一个值,这是一个节省带宽的做法,可以存储一个更短的属性名到数据库,同时在调用的时候保持可读性。

1
2
3
4
5
6
7
8
9
10
11
12
13
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

1
2
3
4
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:

1
2
3
const schema = new Schema({..},{autoIndex:false});
const Clock = mongoose.model('Clock',schema);
Clock.ensureIndexes(callback);

option:bufferCommands

默认情况下,mongoose buffers会在连接中断时发出命令,直到驱动程序重新连接为止。若要禁用缓冲,请将bufferCommands设置为false。

1
const schema = new Schema({..},{bufferCommands:false});

schema 的bufferCommands 会重写全局的 bufferCommands 选项。

1
2
mongoose.set('bufferCommands',true);
const schema = new Schema({..},{bufferCommands:false});

option:capped

Mongoose 支持 MongoDB 的 capped collections ,要从底层把 collection 设定为 capped(封顶),可以把collection 的最大容量设定到 capped 选项(单位 bytes)

1
new Schema({..},{capped:1024});

如果要传递多个选项,capped 也可以作为对象传递.这个试试需要显式传入必要值 size

1
new Schema({..},{size:1024,max:1000,autoIndexId:true})

option:collection

Mongoose 通过 utils.toCollectionName 方法,默认生成 collection 的名称(生成 model 名称的复数形式)。设置这个选项可以自定义名称

1
const dataSchema = new Schema({..},{collection:'data'});

option:id

Mongoose 会默认生成一个虚拟值 id,指向文档的 _id字段。如果你不需要 id 虚拟值,可以通过这个选项禁用此功能。

1
2
3
4
5
6
7
8
9
10
11
// 默认行为
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的文档,如果硬是要这么做,会报错。

1
2
3
4
5
6
7
8
9
10
11
// 默认行为
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 默认不保存空对象。

1
2
3
4
5
6
7
8
9
10
11
12
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 将保存空对象

1
2
3
4
const Sam = new Character({name:'Sam',inventory:{}},{minimize:false});
Character.findOne({name:'Sam'},function(err,character){
console.log(character); // {name: 'Sam',inventory:{}}
});

option:read

1
2
3
4
5
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'
1
2
3
4
5
const options = { replset: { strategy: 'ping' }};
mongoose.connect(uri, options);

const schema = new Schema({..}, { read: ['nearest', { disk: 'ssd' }] });
mongoose.model('JellyBean', schema);

option:shardKey

分片相关.

1
new Schema({ .. }, { shardKey: { tag: 1, name: 1 }})

option:strict

Strict 选项默认为 true,这意味着你不能 save schema 里没有声明的属性。

1
2
3
4
5
6
7
8
9
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() 也受该选项影响:

1
2
3
4
5
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 级别重写,在第二个参数值传入:

1
2
3
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 中不存在的实例上设置的任何键/值总是被忽略,不管模式选项是什么。

1
2
3
4
5
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

1
2
3
4
5
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 });

严格的选择适用于更新

1
2
// Mongoose will strip out `notInSchema` from the update if `strict` is not `false`
MyModel.updateMany({}, { $set: { notInSchema: 1 } });

Mongoose 有一个单独的 strictQuery 选项,用于将过滤器参数的strict模式切换到查询。

1
2
3
4
5
6
7
8
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方法时才适用。

1
2
3
4
5
6
7
8
9
10
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 }

1
2
3
4
5
6
7
8
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

类型声明

1
const schema = new Schema({loc:{type:String,coordinates:[Number]}});

对于像geoJSON这样的应用程序,“type”属性非常重要。控制使用哪个键mongoose来查找类型声明,要设置“typeKey”模式选项

1
2
3
4
5
6
7
8
9
const schema = new Schema({
loc:{
type:String,
coordinates:[Number],
},
name:{
$type:String
}
},{typeKey:'$type'});

option:validateBeforeSave

默认情况下,文档在保存到数据库之前会自动验证。这是为了防止保存无效的文档。如果希望手动处理验证,并能够保存没有通过验证的对象,可以将validatebeforeave设置为false。

1
2
3
4
5
6
7
8
9
10
11
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。如果这个值与你的计划冲突,你可以设定为其他名称:

1
2
3
4
5
6
7
8
9
10
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

1
2
3
4
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

跳过版本控制允许从版本控制中排除路径(例如。,即使更新了这些路径,也不会增加内部修订)。除非你知道你在做什么,否则不要这样做。对于子文档,使用完全限定路径将其包含在父文档中。

1
2
3
new Schema({..}, { skipVersioning: { dontVersionMe: true } });
thing.dontVersionMe.push('hey');
thing.save(); // version is not incremented

option:timestamps

如果设置了 timestamps 选项, mongoose 会在你的 schema 自动添加 createdAtupdatedAt 字段, 其类型为Date。这两个字段的默认名称是 createAtupdateAt,你可以通过设定 timestamps.createAttimestamps.updateAt 自定义名称。

1
2
3
4
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 的严格模式设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
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 设定。

1
2
3
4
5
6
7
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

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
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 属性的对象

1
2
3
4
5
6
7
8
const schema = new Schema({
test:String
});
const schema2 = new Schema({
test:{
type:String
}
});

除了 type 属性,还可以对这个字段路径指定其他属性,如果要在保存之前把字母都改成小写:

1
2
3
4
5
6
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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:布尔值 是否对这个属性创建稀疏索引
1
2
3
4
5
6
7
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 修改了数据。

1
2
3
4
5
6
7
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 或者传入 一个空对象定义。下面三种方法效果是一致的。

1
2
3
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 字段路径

1
2
3
person.anything = {x:[3,4,{y:'changed'}]}
person.markModified('anything');
person.save(); // anything will now get changed

ObjectIds

要指定类型为 ObjectId,在声明中使用 Schema.Types.ObjectId

1
2
3
const mongoose = require('mongoose');
const ObjectId = mongoose.Schema.Types.ObjectId;
const Car = new Schema({driver:ObjectId});

Arrays

创造 SchemaTypes 或者子文档数组

1
2
3
4
5
6
7
const ToySchema = new Schema({name:String});
const ToyBox = new Schema({
toys:[ToySchema],
buffers:[Buffer],
string:[String],
number:[Number]
});

注意:指定空数组相当于 Mixed,下面的操作相当于创建 Mixed数组:

1
2
3
4
const Empty1 = new Schema({ary:{]}});
const Empty2 = new Schema({any:Array});
const Empty3 = new Schema({any:[Schema.Types.Mixed]});
const Empty4 = new Schema({any:[{}]});

数组的默认值是 [](空数组)

1
2
const Toy = mongoose.model('Test',toySchema);
new Toy().toys // []

要手动把默认值设置为 undefined,从而覆盖 []

1
2
3
4
5
6
const ToySchema = new Schema({
toys:{
type:[ToySchema],
defalut:undefined
}
});

创建自定义 Type

Mongoose 可以扩展自定义 SchemaType.

schema.path()函数

这个函数为给定字段路径返回实例化 schema type

1
2
3
4
5
6
7
8
9
10
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

1
mongoose.connect('mongodb://localhost/myapp');

这是连接本地 myapp数据库默认接口(27017)的最小配置。本地连接失败可以尝试 127.0.0.1.local hostname 被修改有时候会引起问题。

可以在 url 中指定多个参数:

1
mongoose.connect('mongodb://username:password@host:port/database?options');

操作缓存

不必等等连接建立成功就可以使用 Mongoose models

1
2
3
4
5
mongoose.connect('mongodb://localhost/myapp');
const MyModel = mongoose.model('Test',new Schema({name:String}));
MyModel.findOne(function(err,res){
// ...
});

Mongoose 会缓存 model 操作。这个操作很方便,但也会引起一些疑惑,因为如果没有连上的话,Mongoose 不会抛错。

1
2
3
4
5
6
7
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:

1
mongoose.set('bufferCommands',false);

选项

connect 方法也接受 options 参数,这些参数会传入底层 MongoDB 驱动

1
mongoose.connect(uri,options);

Mongoose 会不做修改直接把选项传到驱动,以下有一点例外

  • bufferCommands 是 mongoose 特有的选项,
  • user/pass 用于认证的用户名和密码。mongoose 特有的,等价于 MongoDB 驱动的 auth.userauth.password选项
  • autoIndex 理想情况下,mongoose 在连接时会自动建立 schema 索引。这有利于开发,但是在大型生产环境中不是很理想,因为会导致性能下降。
  • dbName 指定要连接的数据库名称(覆盖连接字符串)

下面是一些重要的选项

  • autoReconnect 底层 MongoDB 驱动在连接后将自动重连。除非是管理连接池的高手,不然不建议设置为 false
  • bufferMaxEntries MongoDB 驱动同样有自己的离线缓存机制。如果希望连接错误时终止数据库操作,可以设这个值为 0 以及把 bufferCommands 设为 false.
  • promiseLibrary 设置底层 promise 库
  • poolSize MongoDB 保持的最大 socket 连接数。默认是5.MongoDB 3.4 之前,只允许每个 socket 同时进行一个操作,所以如果有几个缓慢请求卡着后面快的请求,可以尝试增加连接数。

举例子:

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
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 专用选项不能在连接字符串使用

1
2
3
4
5
6
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
});

把选项放在连接字符串的劣势是不便阅读。优势是简短。最佳实践是把区分生产环境和开发环境的选项,socketTimeoutMSconnectTimeoutMS 放在 uri , 把通用的常量如 connectTimeoutMSpoolSize 放在选项对象里。

keepAlive注意事项

对于长期运行的后台引用,启用毫秒级 keepAlive是一个精明的操作。不这么做偶尔会经常收到看似没有什么原因的 connection closed 错误

1
mongoose.connect(uri,{keepAlive:120});

副本集(Replica Set)连接

要连接到副本集,可以使用逗号分隔开,传入多个地址:

1
mongoose.connect('mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database][?options]]' [, options]);

连接到单节点副本集,需要指定 relicaSet 选项

1
mongoose.connect('mongodb://host1:port1/?replicaSet=rsName');

多 mongos 支持

使用高性能分片集群,需要连接多个 mongos(MongoDB Shard) 实例。 在 mongoose 5.x 中, 你在连接多个 mongos 时不需要传入任何特殊选项

1
2
// Connect to 2 mongos servers
mongoose.connect('mongodb://mongosA:27501,mongosB:27501', cb);

多个连接

之前我们了解如何使用 Mongoose 默认连接方法连接到 MongoDB。但有时候我们需要权限不同的多个连接, 或是连接到不同数据库。这个情况下我们可以使用 mongoose.createConnection(), 它接受之前提到的所有参数,给你返回一个新的连接。

1
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 选项调整:

1
2
3
4
5
// With object options
mongoose.createConnection(uri, { poolSize: 4 });

const uri = 'mongodb://localhost/test?poolSize=4';
mongoose.createConnection(uri);

模型(Models)

Models 是从 Schema 编译来的构造函数,它们的实例就代表着可以从数据保存和读取 documents,从数据创建和读取 document 的所有操作都是通过 model 进行的。

1
2
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 的实例,创建它们并保存到数据库非常简单:

1
2
3
4
5
6
7
8
9
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 的默认连接。

1
mongoose.connect('localhost','gettingstarted')

如果自行创建了连接,就需要使用 connection 的 model函数代替 mongoose 的 model函数

1
2
const connection = mongoose.createConnection('mongodb://localhost:27017/test');
const Tank = connection.model('Tank',yourSchema);

查询

用 mongoose 查询文档相当容易,它支持 mongoDB的高级查询语法(rich),查询文档可以使用 model的 find,findById,findOne,where 这些静态方法。

1
Tank.find({size:'small'}).where('createDate').gt(oneYearAgo).exec(callback);

删除

modelremove方法可以删除所有匹配查询条件的文档

1
2
3
Tank.remove({size:'large'},function(err){
if(err) return handleError(err);
});

更新

modelupdate方法可以修改数据库中的文档,不过不会把文档返回给应用层。

如果想更新单独一条文档并且返回给应用层,可以使用 findOneAndIUpdate 方法

文档(Documents)

Mongoose documents代表着MongoDB 文档的一对一映射,每个 document 都是他的 Model 实例。

检索

MongoDB 有很多检索数据的方法。

更新

Document 更新的方法同样也有很多,最基本的 findById

1
2
3
4
5
6
7
8
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'})

1
2
3
4
5
6
7
8
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就很适合我们:

1
Tank.update({_id:id},{$set:{size:'large'}},callback);

如果我们确实需要返回文档,下面的方法更加适合:

1
2
3
4
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() 覆盖整个文档。如果要修改在中间件中被保存的文档,这样就很方便了。

1
2
3
4
5
Tank.findById(id,function(err,tank){
if(err) return handleError();
// otherTank 是 tank 的副本
otherTank.set(tank)
});

子文档(Subdocuments)

子文档是指嵌套在另一个文档中的文档。在 Mongoose 中,这意味着可以在 里面嵌套一个 schema 。Mongoose 子文档有两种不同的概念:子文档数组和单个嵌套子文档。

1
2
3
4
5
6
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 可用的特性,两者主要的不同点是子文档不能单独保存,他们会在他们的顶级文档保存时保存。

1
2
3
4
5
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 中间件同理。

1
2
3
4
5
6
7
8
9
10
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()前的验证就是一个中间件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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就能返回文档数组中特定文档。

1
const doc = parent.children.id(_id);

添加子文档到数组

Mongoose 数组方法有 push、unshift、addToSet 等等:

1
2
3
4
5
6
7
8
9
10
11
12
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 方法可以新建文档但是不加入数组

1
const newdoc = parent.children.create({name:'Haha'});

删除子文档

每个子文档都有 remove 方法,另外,对于子文档数组,有一个等效的方法.pull()。对于单个嵌套子文档,remove 与把这个文档的值设为 null 等效。

1
2
3
4
5
6
7
8
// 等效 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:

1
2
3
4
5
6
7
const parentSchema = new Schema({
children:[{name:'string'}]
})
// 等价于
const parentSchema = new Schema({
children:[new Schema({name:'string'})]
});

查询(queries)

Model 的多个静态辅助方法都可以查询文档。

Model 的方法中包含查询条件参数的(find findById count update)都可以按照下面两种方式执行:

  1. 传入 callback 参数,操作会被立即执行,查询结果被传给回调函数(callback)
  2. 不传 callback 参数,Query 的一个实例(一个 query对象)被返回,这个 query 提供了 构建查询器的特殊接口。

Query 实例有一个 .then()函数,用法类似 promise

如果执行查询时传入 callback,就需要用 JSON 文档的格式指定查询条件,JSON 文档的语法跟 MongoDB shell 一致。

1
2
3
4
5
6
7
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 这个参数会是怎么样的:

1
2
3
4
5
6
7
8
9
10
// 查找每个  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个实例等效:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 的一个实例:

1
2
3
4
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 回调会接受错误
  • 验证是可以定制的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 验证器

上面的链接提供了使用和错误处理相关的详细信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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 的辅助函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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);
});
});

自定义验证器

如果内建检验器不够用了,可以定义满足自己需要的验证器

自定义验证器通过传入一个验证函数来定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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 会将回调函数作为验证函数的第二个参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 属性如果验证器抛出错误,这个属性会包含该错误原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 验证器

定义嵌套对象的验证器需要特别注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 值未定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 才会触发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 验证器 不会对数组自身验证,只会对数组中的元素验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 函数的时候,下一个执行。

1
2
3
4
5
const schema = new Schema({..});

schema.pre('save',function(next){
next();
});

在 5.x 版本中,除了手动调用 next ,可以返回一个 promise ,甚至是 async/await。

1
2
3
4
5
6
7
8
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 () 后面的代码。

1
2
3
4
5
6
7
8
9
10
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');
});

并行

并行中间件提供细粒度流控制

1
2
3
4
5
6
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 。下面是几个错误的处理的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 中间件都已经完成了。

1
2
3
4
5
6
7
8
9
10
11
12
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 触发下一个中间件

1
2
3
4
5
6
7
8
9
10
11
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’)钩子之前调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 为这些函数制定了新钩子。

1
2
3
4
5
6
7
8
9
10
11
12
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 钩子

1
2
3
schema.pre('update',function(){
this.update({},{$set:{updateAt:new Date()}});
});

错误处理中间件

错误处理中间件比普通中间件多一个 error 参数,并且 err 作为第一个参数传入。 而后错误处理中间件可以让你自由地做错误的后续处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 返回的一切对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 的值赋给它就好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

我们做的东西还是跟平时差不多,只是创建了 PersonStory。现在试试对query 填充 story 的 author。

1
2
3
4
5
6
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 数组。

设置被填充字段

1
2
3
4
5
6
7
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 就可以实现。

1
2
3
4
5
6
7
8
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
});

填充多个字段

1
2
3
4
5
Story.
find(...).
populate('fans').
populate('author').
exec();

如果对同一路径 populate 两次,只有最后一次生效。

1
2
3
4
5
6
7
// 第二个会覆盖第一个的
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个数据

1
2
3
4
5
6
7
8
9
10
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() 到数组中。

1
2
author.stories.push(story1);
author.save(callback);

然后我们就能 find 和 populate 了

1
2
3
4
5
6
Person.findOne({name:'Ian Fleming'}).
populate('stories').
exec(function(err,person){
if(err) throw handleError(err);
console.log(person);
});

如果父子节点互相指向,数据可能会在某一时刻失去同步。为此我们可以不使用填充,直接 find()我们需要的 story

1
2
3
4
5
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

1
2
3
4
5
6
7
const userSchema = new Schema({
name:String,
friends:[{
type:ObjectId,
ref:'User'
}]
});

当然可以填充得到用户的 friends 列表,但是如果要在获得他们朋友的朋友呢?指定 populate选项就可以了

1
2
3
4
5
6
7
User.findOne({name:'Val'}).
populate({
path:'friends',
populate:{
path:'friends'
}
})

跨数据库填充

假设现在有 event schema 和 conversation schema,每个 event 对应一个 conversation 线程

1
2
3
4
5
6
7
const eventSchema = new Schema({
name:String,
conversation:ObjectId
});
const conversationSchema = new Schema({
numMessage:Number
});

并且,event 和 conversation 保存在不同的 MongoDB 实例中

1
2
3
4
5
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

1
2
Event.find().population({path:'conversation',model:Conversation}).
exec(function(err,docs){});

动态引用

Mongoose 也可以从多个 collection 填充。假设 user schema 有一系列 connection,一个 user 可以连接到其他 user 或者组织。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的实例。

1
2
3
4
5
6
7
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 ~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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);

###

0%