首页 专题 H5案例 前端导航 UI框架

Mongoose开发实战-进阶篇

作者:TG 日期: 2017-11-10 阅读: 1720
在上一篇《Mongoose开发实战-基础篇》中,我们了解了如何连接数据库和实现基本的CRUD操作,今天我们来了解一些有趣且实用的功能。

知识点:
  1. 索引
  2. 验证器
  3. 联表查询
  4. 虚拟属性
  5. 中间件
  6. 插件Plugins

1. 索引

索引可以加快查询速度,我们通过一个例子来看看效果。

在mongo Shell中,我们创建10000条数据:

$ mongo

> for (var i = 0; i < 10000; i++) { //

... db.users.insert({'name': 'user' + i}); //

... }

看看未加索引的情况下查询:

> db.users.find({'name': 'user1000'}).explain()

留意nscannedmillis,分别表示查询的条数和时间(ms)

现在我们来添加索引后执行查询:

> db.articles.ensureIndex({name: 1});

> db.articles.find({'name': 'user1000'}).explain()

从上面的实例可以看出,加了索引后,能快速的查询一条,这可以看到索引极大的提升了查询速度。

下面我们看看如何使用Mongoose创建:

// 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});


注:需要注意的是,当应用启动的时候, ,Mongoose会自动为Schema中每个定义了索引的调用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 }); //不推荐


注:对于添加的每一条索引,每次写操作(插入、更新、删除)都将耗费更多的时间。这是因为,当数据发生变化时,不仅要更新文档,还要更新集合上的所有索引。因此,mongodb限制每个集合最多有64个索引。通常,在一个特定的集合上,不应该拥有两个以上的索引。

2. 验证器Validate

验证器规则
  • 验证是在SchemaType上定义的。
  • 验证是中间件。Mongoose 验证器作为pre('save')前置钩子在每个模式默认情况下执行。
  • 你可以手动使用document运行验证。validate(callback)doc.validateSync()
  • 验证程序不运行在未定义的值上,除了required验证器。
  • 验证异步递归;当你调用Model#save,子文档验证也可以执行。如果出现错误,你的 Model#save回调接收它。
  • 验证是可自定义的。

(1)内置验证器

Mongoose提供了几个内置验证器。
  • 所有的SchemaType都有内置的require验证器。所需的验证器使用SchemaTypecheckrequired()函数确定值是否满足所需的验证器。
  • 数值( Numbers )有最大(man)和最小(min)的验证器。
  • 字符串(String)有enummatchmaxLengthminLength验证器。

下面我们创建一个用户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


如有任何疑问或意见,欢迎在下方的评论区留言!


关注”全栈技术杂货铺“

全栈技术杂货铺