blog
blog copied to clipboard
我的Mongoose快速使用指南
前言
mongoose是nodejs圈里知名的mongodb库,不但可以操作mongodb,还可以做mongodb的ODM。简化bson与model之间的转换。mongoose功能很强大,官网文档很详细,不过为了减少查找的麻烦,本文根据使用经验,mark几个常用的使用方法。
背景
接到任务要开发一个内部的KPI打分系统,产品经理、项目经理、技术经理之间互相打分,这帮人又都能给研发和测试打分。(-_-!!!,接到这个任务时,心里一万只***奔过,费力不讨好,还把自己往火坑里跳)其实之前只用nodejs做过一个opennebula的nodejs client库(但中止了)。但因为功能这个KPI系统功能少,加之希望未来前端组能接手,所以毅然决然得选择了nodejs(现在看来,选择nodejs又是给挖坑埋自己),web框架用的express,数据库想了一下(考虑到需求不明确,领导想法一时一变),选择了mongodb(相当明智!)。 nodejs连接mongodb有官网推荐native-driver,但我当时犯懒,于是找到了支持ODM的mongoose框架。
ODM下model定义
定义schema
mongoose官方首页提供了一种model定义的方式:
var mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/test');
var Cat = mongoose.model('Cat', { name: String });
var kitty = new Cat({ name: 'Zildjian' });
kitty.save(function (err) {
if (err) // ...
console.log('meow');
});
定义model可以使用mongoose.model()函数,第一个参数’Cat’是model的类名,后面的参数是<属性名, 属性类型>的map。这个属性map其实是mongoose里的一个Schema,当属性多时一般是显式定义mongoose.Schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var blogSchema = new Schema({
title: String,
author: String,
body: String,
comments: [{ body: String, date: Date }],
date: { type: Date, default: Date.now },
hidden: Boolean,
meta: {
votes: Number,
favs: Number
}
});
var Blog = mongoose.model('Blog', blogSchema);
所以要定义model,第一步就是定义Model的Schema(mongodb中实际存储的数据格式),而且由于mongodb自身的特点,定义schema时允许嵌套属性(如上面的meta属性)。只要是bson支持,schema就可以定义。
定义methods
model的methods分为两种,一种是实例的,一种是类的。在定义时有所区分:
// 定义实例methods
var animalSchema = new Schema({ name: String, type: String });
animalSchema.methods.findSimilarTypes = function (cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
// 或者
animalSchema.methods = {
findSimilarTypes: function(cb) {
return this.model('Animal').find({ type: this.type }, cb);
}
}
// 定义类methods
animalSchema.statics = {
findByName: function(cb) {
return this.find({ name: new RegExp(name, 'i') }, cb);
}
}
深入schema
属性读取开关 - select
默认在从mongodb中读取数据时会获取该document的所有属性,但有时不想拿到一些属性,比如UserSchema中会有hashed_passwd和salt值,这些值在获取用户详情时是用不到的,而且也不应该读出来。mongoose在schema定义时为属性提供一个select attribute,如果设为false,则获取document时不会读取(默认是true)。
var UserSchema = new Schema({
loginname: { type: String, default: '' },
name: { type: String, default: '' },
salt: { type: String, *select: false*, default: '' },
hashed_password: { type: String, *select: false*, default: '' },
role: { type: Number, default: 128 },
mixed: Schema.Types.Mixed
});
属性读写hook - virtual
大家在开发过程中应该会遇到这两种情况:
- 外部接口/页面需要的数据并不是直接存储在数据库里,而是由数据库中多项属性拼接后的数据。比如:用户的全名,数据库中存 姓 和 名,两个字段,前端则直接将 姓 和 名 拼接起来后显示
- 某个属性在从数据库中读出来后需要做一些处理再显示,或者写到数据库前必须做一下处理
针对以上情况,mongoose提供了virtual属性(其实是个函数)功能,允许开发者在已有的schema中添加virtual属性。注:virtual属性是不会存储到mongodb中的!
还以上面的UserSchema为例,用户注册时会填写明文(或base64编码后)密码,后台接收到注册请求后,首先生成密码的salt,然后用某个算法生成hashed密码,然后存储hashed密码(千万别存明文密码!)。mongoose为这套流程提供了一个非常好的方案:定义一个virtual password属性。
UserSchema.virtual('password')
.set(function(password) {
this.salt = this.makeSalt();
this.hashed_password = this.encryptPasswd(password, this.salt);
})
.get(function() {
return '';
});
这段代码为UserSchame定义了password virtual属性,并为该属性定义了getter和setter方法。setter方法接收明文密码作为参数,然后生成salt,并设置了自身实例的hashed_passwd属性(即加密后密码)。这个setter方法会在UserSchema的实例调用password = “123456”时调用,这样就减少了业务逻辑的代码,也不用在注册和修改密码时写两套代码了。model层面能解决的,尽量别放到业务逻辑层去搞!
属性validate
在将数据存储到mongodb前,必须保证数据的有效性。在定义schema时,mongoose三种简单的validate方式:
- 可以指定某个属性的required: true
- 数字属性可以指定min和max验证
- 字符串属性可以指定enum、match、maxlength和minlength验证
除此之外,mongoose还在schema定义之外提供了属性的复杂validate功能:
UserSchema.path('loginname') \\ 对loginname属性进行非空验证
.validate(function(loginname) {
return !_.isEmpty(loginname);
}, 'login name cannot be empty');
以mixed为基础的schema-free
mongodb的一大优势就是schema-free,但我在用mongoose这种mongodb的ODM框架时,时常让我发生错觉:这tm就是在用一个mysql!
因为定义model时需要先定义schema,schema每一项属性都是确定的,属性名称是什么,属性的类型是什么—这跟sqlalchemy+mysql的定义毫无区别。哪里可以体现mongodb的schema-free呢?
看官方文档,最终发现了schema-free的利器:Schema.Types.Mixed!
”talk is cheap, show me the code!”
var projectSchema = new Schema({
projectname: { type: String, default: '' },
projectnumber: { type: String, default: '' },
// 1: Product; 2: Marketing; 4: Technical
projecttype: { type: Number, default: 0 },
date: { type: Date, default: Date.now },
projectmanger: { type: Schema.Types.ObjectId, ref: 'User' },
productmanger: { type: Schema.Types.ObjectId, ref: 'User' },
presales: { type: Schema.Types.ObjectId, ref: 'User' },
technicalmanger: { type: Schema.Types.ObjectId, ref: 'User' },
// Add:
// project scores=>>{"prjscore": 90}
mixed: Schema.Types.Mixed
});
我在做这个系统时,前面几次需求都没谈论到项目得分这个东西,但后来领导突然说要加这个功能,算作是产品经理对项目经理打得分,项目内所有任务的得分都需要乘以项目得分/120这个系数。。。
还好我早有预见,在我定义的所有model中都加了一个mixed属性,属性类型是:Schema.Types.Mixed(或者直接写成{}),然后往mixed里面加了一个prjscore属性:
// 错误用法
project.mixed.prjscore = 90
// 或 project.mixed = {prjscore: 90}
project.save()
看上去非常易用,但实际上你真的这么用,prjscore属性是不能写到mongodb里的。必须在project.save()前加一行project.markModified("mixed"),告诉mongoose mixed属性已经变化,这才会写入到mongodb中,正确的用法是:
// 正确用法
project.mixed.prjscore = 90
// 或 project.mixed = {prjscore: 90}
project.markModified("mixed")
project.save()
总结
以上总结了mongoose的一些基本用法,使用起来还是非常方便的,“以后还会用”!
Mongoose 4.x 后可以直接
project.mixed.set('prjscore', 90)
project.save()
虽然 MongoDB 支持范式(集合关联),查询时可以用 populate ,但性能真的是贼慢
比如我这边某个集合有5千多条数据,MongoDB查询给我花了 300ms 左右,害怕。。。
后来加了一层 redis 缓存才解决。(当然最好的解决办法当然是硬件提升啦
虽然觉得 Mongo 会比 sql 慢,但 hook , virtual 等用起来挺爽的
😆 翻了下博主主页,原来是后端大牛 6666
前端菜鸟过来学习~
你给的 FAQ 链接里,并没有提到 Mixed 类型,只说了Array 类型。
// 3.2.0
doc.array.set(3, 'changed');
doc.save();
// if running a version less than 3.2.0, you must mark the array modified before saving.
doc.array[3] = 'changed';
doc.markModified('array');
doc.save();
对。。我搞错了,测试了下 Mixed 类型不能这么用