MongoDB查询以及投影操作符的实践

当使用MongoDB提供复制操作时,需要简单的CURD操作基础上使用一些操作符(如同$eq这样的操作符),mongoDB官网文档[1]有其详细介绍。需要注意的是,Node端所使用的Mongoose工具,在API上与原生的驱动还是有细微的区别。,本文结合mean.js(MongoDB, Express, AngularJS, and Node.js)开发实例来学习查询操作符的使用,先介绍做项目涉及到的操作然后结合实践中的涉及的内容。


1. 比较操作符

比较查询操作符符则有两类,一类是大于($gt、$gte)、小于($lt、$lte)、等于($eq、$ne)这种2值比较,一类是是否与数组内元素匹配($in、$nin)。当数值型数据进行比较时,MongoDB会先转换再进行比较。

2. 逻辑操作符

逻辑查询操作符有4种,分别是:与($and)、或($or)、非($not)以及或非($nor)逻辑。$and、$or、$nor后接大括号来框住的所接受的条件。而$not的使用有点不同,需要在指定字段之后,可以匹配后接的子条件以及该字段不存在的情况。

3. 元素操作符

元素选择器中$exists就是来选择指定字段存在数值的文档,$type则是选择字段的数值类型为指定类型,BSON当中的数据类型都有相应的数字来表示,这种类型数字就是在这里使用了。

4. 数组操作符

这3种针对数据选择的操作器如字面意思理解就可以了。需要注意几点就是:$all相当于$and 的行为一样,也就是数组中的元素条件要同时满足所以条件,而$elemMatch则需要对象数组中满足$elemMatch条件的所有条件,并且只返回第一个匹配的数组元素,这里还可以联系依稀$in选择器,$in限定来选择条件为一组值来匹配数组,而$elemMatch匹配的是对象数组元素。

5. 投影操作符

投影(project)的功能可以与SQL中的select类比,满足此条件的文档只返回包含指定字段的结果集。位置$符来匹配查询条件的第一个文档,$一般伴随着其他条件来限定数组元素的获取。比如这个条件 { semester: 1, grades: { $gte: 85 } }, { "grades.$": 1 } 就是在满足前面两个条件下,只获取grades数组第一个匹配的元素。$elemMatch如上述的应用一样返回符合匹配的第一个元素。

$meta的意义比较有意思,它的格式定义为{ $meta: }, 里头的metaDataKeyword指定'textScore'为关键词。这里需要引入$text文本搜索(其格式为{ $text: { $search: , $language: } })来设置搜索文本的查询条件,引入来TextScore来评价文档与搜索关键词的关联性。当$meta指定'textScore'时,查询结果就是文本相关的分数。

$slcie就如同数组操作slice一样,只返回指定数量、范围的元素,如db.posts.find( {}, { comments: { $slice: [ -20, 10 ] } } ),就只返回从comments数组倒数20开始的前10个元素。

照本宣科就到这里,还有一些类别(Evaluation、Geospatial)的操作符没有提到,是因为这些类别暂时没有使用过。下面汇总项目中使用到的查询实例,以便项目接盘侠能轻松上手。


1. 针对数组查询案例

第一个场景是数据库集合中存储了数据处理的两个状态,一个是公共编辑的状态('commonStatus'),一个是用户编辑的私有状态('userStatus')。业务上需要通过这两种状态的组合获取数据,比较麻烦的地方就是用户私有的信息需要涉及数组操作。具体数据结构如下:

commonStatus: {
  type: String
},
privateInfo: [{
  _creator: {
    type: Schema.Types.ObjectId, ref: 'User'
  },
  score: {
    type: Number
  },
  scoredTime: {
    type: Date
  },
  userStatus: {
    type: String
  },
  tag: [{
    type: Schema.Types.ObjectId, ref: 'User.customTags'
  }]
}],

私有状态的业务的查询是这样的,前端传递'userId'和'userStatus'给Node端,当'userStatus'为'unchecked'时在数据库中可能的情况有3种:第一种情况为'privateInfo'存在当前用户其userStatus为'unchecked',第二种情况为privateInfo数组存在但没有当前用户数据,第三种情况为privateInfo数组为空的情况,也就是集合中文档的缺省值。查询语句的编写如下代码

//构造查询语句,使用$or操作符包含3中可能情况
queryCondition.$or = [{
  //实际不可能出现的情况,但保证查询的robust
  //条件一: privateInfo存在并有userId的数据其userStatus为'unchecked'
  'privateInfo': {
    //使用$all限定指定字段的数据
    //不使用$all则严格限定privateInfo数据格式如查询条件
    $all: {
      '_creator': new ObjectId(condition.userId),
      'userStatus': {
        //使用$nin排除有其他意义的状态
        $nin: ['scored', 'communicated'] 
      }
    }
  }
}, {
  //条件二: privateInfo 存在但没有userId的相关数据包括无数据
  'privateInfo._creator': {
    //使用$ne排除包含用户数据的数组
    $ne: condition.userId
  }
}, {
  //条件三: 初始邮件,privateInfo无数据
  //通过$size限定数组的大小匹配空数组
  'privateInfo': { $size: 0 }
}];

这样就将'unchecked'状态囊括了,但其他私有条件还进行不同条件的查询,如下所示,前端传递个Node端私有状态的值提供查询。

if(condition.userStatus === 'unchecked'){
  //如上
}else{
  //其他私人状态只需满足上述条件一的形式
  queryCondition.privateInfo = {
    $all: [{
      //将$elemMatch与$all结合
      //当数据需要满足多种$elemMatch组合时
      $elemMatch: {
        '_creator': condition.userId,
        'userStatus': condition.userStatus
      }
    }]
  };
}

2. 时间范围查询案例

接下来的案例是获取时间时大于指定时间的情况,直接使用$gte比较操作符就可以来,

Mails.find({
  'mail.date': {
    $gte: contactDate
  },
  'mail.from': {
    $elemMatch: {
      'address': fromAddr
      }
    }
  })
  .select('_id mail.subject').sort({'mail.date': -1}).lean().exec(function(err, mails){
    //后续操作
  });

总结

本文简单罗列来mongoDB的查询以及投影操作符的含义并解释来两个实例应用。其实还有一些需要实践经验才能知道的,比如参考后两个就是比较典型的例子,总体来说mongoDB操作符不难,多多实践就可以来。mongoDB下一部分为更新操作符涉及来数组的操作以及$符的占位作用等,这些都在暑假实践过,更加有意思。

参考

  1. 官网文档的操作符介绍

  2. Mongoose内嵌对象查询

  3. Mongose对象数组查询