Mongoose开发实战-进阶篇
- 索引
- 验证器
- 联表查询
- 虚拟属性
- 中间件
- 插件Plugins
$ mongo
> for (var i = 0; i < 10000; i++) { //
... db.users.insert({'name': 'user' + i}); //
... }
> db.users.find({'name': 'user1000'}).explain()
![](https://imagecloud.laixiazheteng.cn/mongodb-no-index.png)
nscanned
和millis
,分别表示查询的条数和时间(ms)> db.articles.ensureIndex({name: 1});
> db.articles.find({'name': 'user1000'}).explain()
![](https://imagecloud.laixiazheteng.cn/mongodb-index.png)
// modules/articles/articles.model.js
const ArticlesSchema = new Schema({
title: {
...
index: true
}
}, {collection: 'articles'});
const ArticlesSchema = new Schema({
title: {
...
index: true,
unique: true
}
}, {collection: 'articles'});
ArticlesSchema.index({ name: 1});
//1 表示正序, -1 表示逆序
ArticlesSchema.index({name: 1, by: -1});
ArticlesSchema.index({name: 1, by: -1}, {unique: true});
ensureIndex
,确保生成索引,并在所有的ensureIndex
调用成功或出现错误时,在 Model 上发出一个'index
'事件。 开发环境用这个很好, 但是建议在生产环境不要使用这个。ensureIndex
:mongoose.connect('mongodb://localhost/blog', { config: { autoIndex: false } }); //推荐
// or
mongoose.createConnection('mongodb://localhost/blog', { config: { autoIndex: false } }); //不推荐
// or
animalSchema.set('autoIndex', false); //推荐
// or
new Schema({..}, { autoIndex: false }); //不推荐
- 验证是在SchemaType上定义的。
- 验证是中间件。Mongoose 验证器作为
pre('save')
前置钩子在每个模式默认情况下执行。 - 你可以手动使用
document
运行验证。validate(callback)
或doc.validateSync()
。 - 验证程序不运行在未定义的值上,除了
required
验证器。 - 验证异步递归;当你调用
Model#save
,子文档验证也可以执行。如果出现错误,你的 Model#save回调接收它。 - 验证是可自定义的。
- 所有的
SchemaType
都有内置的require
验证器。所需的验证器使用SchemaType
的checkrequired()
函数确定值是否满足所需的验证器。 - 数值( Numbers )有最大(
man
)和最小(min
)的验证器。 - 字符串(String)有
enum
,match
,maxLength
和minLength
验证器。
Schema
,给不同的字段添加验证器:// modules/users/users.model.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const UsersSchema = new Schema({
name: {
type: String,
required: true,
minlength: 3,
maxlength: 6
},
age: {
type: Number,
min: 18,
max: 30,
required: true
},
sex: {
type: String,
enum: {
values: ['male', 'female'],
message: '`{PATH}` 是 `{VALUE}`, 您必须确认您的性别!'
},
required: true
}
}, {collection: 'users'});
mongoose.model('users', UsersSchema);
在上面的代码中,name是必填项,且最小长度为3,最大长度为6;age是必填项,最小是18,最大是30;sex是必填项,且必须是male或female。
如果你认真看上面的代码,相信你也看到了{PATH}
和{VALUE}
,这是什么呢?
其实在验证器的错误提示(message
)中,有5个内置变量供我们使用:
{PATH}
: 键名-
{VALUE}
: 当前键值 {TYPE}
:验证器类型,比如min,regexp等{MIN}
: 最小值,只存在数值{MAX}
:最大值,只存在数值
对于内置验证器,我们还可以自定义它的错误提示信息,比如:
min: [6, "自定义错误提示"]
required: [true, "必须项"]
(2)自定义验证器
下面我们创建一个手机验证器:
// modules/common/validation.js
module.exports = {
phone(v) {
return /1[3|5|8]\d{9}/.test(v);
}
};
我们往上面的UsersSchema
中添加一个phone
字段,然后添加自定义验证器:
// modules/users/users.model.js
...
const validation = require('../common/validation');
const UsersSchema = new Schema({
...
phone: {
type: String,
validate: {
validator: validation.phone,
message: '`{PATH}` 必须是有效的11位手机号码!'
},
required: true
}
}, {collection: 'users'});
...
我们还可以使用数组来添加多个验证器:
[
{
validator: validation.phone,
message: '`{PATH}` 必须是有效的11位手机号码!'
}
]
(3)错误提示
当验证失败后,Errors返回一个错误的对象实际上是ValidatorError
对象。每个ValidatorError
都有kind, path, value, message
属性,我们可以拿到每一个错误信息:
const user = new UsersModel(req.body);
user.save((err, result) => {
if (err) {
console.error(err.errors['name']['message']);
return res.status(400)
.send({
message: err
});
} else {
res.jsonp(user);
}
})
我们还可以异步拿到验证错误:
const user = new UsersModel();
const errors = user.validateSync();
除了在Schema上添加验证器,我们还可以使用validate()
方法:
UsersSchema.path('phone').validate(validation.phone, '`{PATH}` 必须是有效的11位手机号码!');
在上面的代码中,我们给字段phone添加validation.phone手机验证器,同时添加错误提示,作用和在Schema上定义一样。
默认情况下,验证器都是只有save
操作才会触发,但是在Mongoose 4.x后,我们也可以开启update()和findoneandupdate()的验证器,只需将runValidators
设为true(默认是false):
const opts = { runValidators: true };
UsersModel.update({}, { name: 'Superman' }, opts, (err) => { });
注意:update验证器只运行在* $set * $unset * $push (>= 4.8.0) * $addToSet (>= 4.8.0) * $pull (>= 4.12.0) * $pullAll (>= 4.12.0)
3. 联表查询
如果你使用过MySql,肯定用过join
,用来联表查询,但Mongoose中并没有join
,不过它提供了一种更方便快捷的方法:Population
(Mongoose >= 3.2 )。
用简短的话来概括Population
的使用:在一个Collection(articles)中定义一个指向另一个Collection(users)的_id字段的字段(by)
const ArticlesSchema = new Schema({
...
by: { type: Schema.Types.ObjectId, ref: 'users' },
...
}, { collection: 'articles' });
mongoose.model('articles', ArticlesSchema);
const UsersSchema = new Schema({
...
}, {collection: 'users'});
mongoose.model('users', UsersSchema);
注意:ref
的值是模型(model
)名称,而不是Collection名称。
当使用populate()
方法时,Mongoose会自动将查询到的值插入到对应的字段中。比如我们要查询一篇文章的作者:
// modules/articles/articles.controller.js
exports.getAuthorByArticleid = (req, res) => {
ArticlesModel.findById(req.query.id)
.populate('by')
.exec(function (err, story) {
if (err) {
return res.status(400).send({
message: '更新失败',
data: []
});
} else {
res.jsonp({
data: [story]
})
}
});
};
查询到的值会插入到by字段中:
{"data":[{"_id":"5a02d5c41f76646d9d369628","title":"123","content":"123","by":{"_id":"5a02d4831515cd6a62a3bc65","name":"Hot","phone":"13123123123","age":21,"sex":"male","__v":0},"articleId":"hfceahcc","__v":0,"modifyOn":"2017-11-08T10:00:36.962Z"}]}
你还可以指定第二个参数来返回指定的值:
populate('by', 'name')
// {"data":[{"_id":"5a02d5c41f76646d9d369628","title":"123","content":"123","by":{"_id":"5a02d4831515cd6a62a3bc65","name":"Hot"},"articleId":"hfceahcc","__v":0,"modifyOn":"2017-11-08T10:00:36.962Z"}]}
返回多个值:
populate('by', 'name phone')
// {"data":[{"_id":"5a02d5c41f76646d9d369628","title":"123","content":"123","by":{"_id":"5a02d4831515cd6a62a3bc65","name":"Hot","phone":"13123123123"},"articleId":"hfceahcc","__v":0,"modifyOn":"2017-11-08T10:00:36.962Z"}]}
不返回某些值(键名前面加-):
populate('by', 'name -_id')
// {"data":[{"_id":"5a02d5c41f76646d9d369628","title":"123","content":"123","by":{"name":"Hot"},"articleId":"hfceahcc","__v":0,"modifyOn":"2017-11-08T10:00:36.962Z"}]}
我们还可以对返回的关联表的数据进行一些处理:
populate({
path: 'by',
match: { age: { $gte: 21 }},
select: 'name',
options: { limit: 5 }
})
上面的代码表示,查询age小于等于21,只显示name字段,且最多5条数据。
4. 虚拟属性VirtualType
虚拟属性并不会存储到MongoDB中,利用它,我们可以格式化和自定义组合属性值。
我们往users中添加一个adresss:
// modules/users/users.model.js
const UsersSchema = new Schema({
...
address: {
city: {type: String},
street: {type: String}
}
}, {collection: 'users'});
如果我们要获取完整的地址,以前我们是一个个拼接,但现在我们可以定义虚拟属性,只需一字获取:
const address = UsersSchema.virtual('address.full');
address.get(function () {
return this.address.city + ' ' + this.address.street;
});
建立一个访问路由:
app.route('/api/users/address')
.get(usersController.getAddress);
getAddress方法:
exports.getAddress = (req, res) => {
UsersModel.findById(req.query.id, (err, result) => {
if (err) {
return err.status(400).send({
message: '用户不存在',
data: []
});
} else {
console.log(result);
res.jsonp(result.address.full);
}
})
}
当你调用这个接口时,你会看到控制台中的输出中并没有full
这个属性,但是我们可以获取到它,这就是虚拟属性。
还有set
方法:
address.set(function(v) {
const split = v.split(' ');
this.address.city = split[0];
this.address.street = split[1];
});
通过定义set
方法,我们可以给两个字段快速赋值:
const body = {
name: 'abcd',
phone: '13123123123',
age: 20,
sex: 'male'
};
const user = new UsersModel(body);
user.address.full = 'beijing 100号';
user.save();
5. 中间件
中间件(也称为前置和后置钩子)是异步函数执行过程中传递的控制的函数。中间件
是在schema
级别上指定的。在我们后续讲解的插件中,中间件也是很重要的。
Mongoose 4.0 有2种类型的中间件:文档(document)中间件和查询(query)中间件。
文档(document)中间件支持以下文档方法:
- init
- validate
- save
- remove
查询(query)中间件支持一下模型和查询方法:
- count
- find
- findOneAndRemove
- findOneAndUpdate
- update
文档(document)中间件和查询(query)中间件支持前置
和后置
钩子。
(1)Pre (前置钩子)
有两种类型的前置钩子,串行(serial)
和并行(parallel)
。
Serial (串行)
串行中间件是一个接一个的执行,只有前一个中间件里调用next()函数,后一个中间件才会执行。
var schema = new Schema(..);
schema.pre('save', function(next) {
next();
});
Parallel (并行)
直到完成每个中间件才会去执行钩子方法里的操作。
var schema = new Schema(..);
schema.pre('save', true, function(next, done) {
next();
setTimeout(done, 100);
});
错误处理:如果任何中间件调用next或done一个类型错误的参数,则流被中断,并且将错误传递给回调。
(2)后置中间件(Post middleware)
后置中间件被执行后,钩子的方法和所有的前置中间件已经完成。后置钩子是是一种来为这些方法注册传统事件侦听器方式,可以看作是一种操作完成后的提示。
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);
});
6. 插件
我们是可以通过插件形式来拓展Schema的功能。
比如文章,当我们修改文章时,一般都会添加一个最后编辑时间,虽然我们可以每次修改时都手动更新,但是我们可以通过插件来自动更新。
// modules/common/plugins.js
module.exports = {
lastModified(schema, options) {
schema.add({ lastMod: Date });
schema.pre('save', function (next) {
this.lastMod = new Date;
next()
})
}
}
// modules/articles/articles.model.js
const plugins = require('../common/plugins');
ArticlesSchema.plugin(plugins.lastModified);
当你点击页面的update按钮,你会发现修改的文章文档已经添加了lastMod字段,且每次修改都会自动更新:
//{ "_id" : ObjectId("5a02d5c41f76646d9d369628"), "title" : "4321", "content" : "123", "by" : ObjectId("5a02d4831515cd6a62a3bc65"), "articleId" : "hfceahcc", "modifyOn" : ISODate("2017-11-08T10:00:36.962Z"), "__v" : 0, "lastMod" : ISODate("2017-11-09T03:36:11.027Z") }
plugin()方法还可以传入第二个参数(用于第一个参数(plugins.lastModified)中传递的第二个参数options),用于传递额外参数:
ArticlesSchema.plugin(plugins.lastModified, {index: true});
module.exports = {
lastModified(schema, options) {
...
console.log(options.index);
}
}
全局的Plugins
mongoose单独有一个plugin()
功能为每一个schema注册插件:
const plugins = require('../common/plugins');
mongoose.plugin(plugins.lastModified);
总结
通过这篇文章,我们掌握的知识点:
- 如何建索引、唯一索引、复合索引
- 了解了如何使用内置验证器、自定义验证器、获取验证错误提示
- 高效联表查询
- 利用虚拟属性来格式化和组合数据
- 利用中间件和插件来集成复用代码
参考文档:
验证器:http://mongoosejs.com/docs/validation.html
联表查询:http://mongoosejs.com/docs/populate.html
虚拟属性:http://mongoosejs.com/docs/api.html#virtualtype_VirtualType
中间件:http://mongoosejs.com/docs/middleware.html
插件:http://mongoosejs.com/docs/plugins
如有任何疑问或意见,欢迎在下方的评论区留言!