imjoey / blog

My Blog based on Github Issues.
286 stars 44 forks source link

我的Mongoose快速使用指南 #7

Open imjoey opened 7 years ago

imjoey commented 7 years ago

前言

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方式:

除此之外,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的一些基本用法,使用起来还是非常方便的,“以后还会用”!

hwen commented 7 years ago

Mongoose 4.x 后可以直接

project.mixed.set('prjscore', 90)
project.save()

虽然 MongoDB 支持范式(集合关联),查询时可以用 populate ,但性能真的是贼慢

比如我这边某个集合有5千多条数据,MongoDB查询给我花了 300ms 左右,害怕。。。

后来加了一层 redis 缓存才解决。(当然最好的解决办法当然是硬件提升啦

虽然觉得 Mongo 会比 sql 慢,但 hook , virtual 等用起来挺爽的

hwen commented 7 years ago

😆 翻了下博主主页,原来是后端大牛 6666

前端菜鸟过来学习~

imjoey commented 7 years ago

@hwen 我刚翻看 Mongoose v4.11.1 的文档,里面依然提到需要调用 markModified 函数做标记哦。

请教你是在哪里看到不需要标记的?能否发个文档链接上来。谢谢。

hwen commented 7 years ago

这里 FAQ,第一项就是

记错了,是 3.2 之后就可以了

觉得 Mongoose 的文档写得不是很好,FAQ 里写的东西我也没在其他地方找到 。。。

imjoey commented 7 years ago

你给的 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();
hwen commented 7 years ago

对。。我搞错了,测试了下 Mixed 类型不能这么用