Mongoose开发实战-进阶篇

6,706 阅读5分钟
在上一篇《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);


总结


通过这篇文章,我们掌握的知识点:

  • 如何建索引、唯一索引、复合索引
  • 了解了如何使用内置验证器、自定义验证器、获取验证错误提示
  • 高效联表查询
  • 利用虚拟属性来格式化和组合数据
  • 利用中间件和插件来集成复用代码


参考文档:

验证器:mongoosejs.com/docs/valida…

联表查询:mongoosejs.com/docs/popula…

虚拟属性:mongoosejs.com/docs/api.ht…

中间件:mongoosejs.com/docs/middle…

插件:mongoosejs.com/docs/plugin…


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