面向Node.js开发者的MongoDB教程(3):查询

854 阅读5分钟

说明 本文中使用的数据库是教程提供的bilibili_hot数据库,可以通过前文《MongoDB和Mongoose基础》查看如何下载并导入教程提供的数据。

  • MongoDB的相关操作是在mongosh进行操作
  • Mongoose相关操作是在已express.js为基础的Node.js后端服务中进行操作
  • 云服务器中使用MongoDB

MongoDB-查询

mongosh提供了多个用于查询的方法,可以满足查询文档、查询并更新文档的需求。

  • db.collection.find()
  • db.collection.findAndModify()
  • db.collection.findOne()
  • db.collection.findOneAndDelete()
  • db.collection.findOneAndReplace()
  • db.collection.findOneAndUpdate()

让我们在看一下db.collection.find方法的方法体本身,在mongosh中输入db.hotspots.find并回车,可以看到以下内容:

function (query, fields, limit, skip, batchSize, options) {
    var cursor = new DBQuery(this._mongo,
                             this._db,
                             this,
                             this._fullName,
                             this._massageObject(query),
                             fields,
                             limit,
                             skip,
                             batchSize,
                             options || this.getQueryOptions());

    {
        const session = this.getDB().getSession();

        const readPreference = session._serverSession.client.getReadPreference(session);
        if (readPreference !== null) {
            cursor.readPref(readPreference.mode, readPreference.tags);
        }

        const readConcern = session._serverSession.client.getReadConcern(session);
        if (readConcern !== null) {
            cursor.readConcern(readConcern.level);
        }
    }

    return cursor;
}

基础用法

  1. 查询全部文档:

find()方法的第一个参数query用于指定查询条件,决定了要返回哪些文档。要是不指定,默认就是{},此时会返回集合中的所有文档。

> db.hotspots.find()
  1. 查询指定值:

将键值对的对象作为第一个参数传入,就可以查找指定值的文档:

  • 查找指定title
> db.hotspots.find({ "title": "酒窖?超级无敌持续战斗状态!" })
  • 查找指定aid
> db.hotspots.find({ "aid": 890051687 })
  1. 查询内嵌文档:

find()方法也支持查询内嵌文档,例如我们来搜索名字为"文武俩兄弟"的UP主:

db.hotspots.find({ "owner.name": "文武俩兄弟" })
  1. 正则匹配:

MongoDB使用Perl兼容的转增则表达式(PCRE)库来匹配正则表达式,任何PCRE支持的正则表达式语法都能被MongoDB接受。让我们查找一下标题以!结尾的视频吧:

> db.hotspots.find({ "title": /!$/ })

返回指定键

find()方法的第二个参数fields用于指定需要返回的键,例如我们只关心title的值,不需要返回所有的文档,我们可以传入第二个参数。

> db.hotspots.find({ "title": /!$/ }, { "title": 1 })

如果你查看返回的文档,我相信你会看到"_id"这个键是被默认返回了,如果你不希望返回"_id",可以将其设置为0

> db.hotspots.find({ "title": /!$/ }, { "title": 1, "_id": 0 })

分页和排序

通过limitskip我们可以进行分页操作,这在平时的Web开发中是经常被使用到的。

limit

find()方法的第三个参数为limit,我们可以传入我们想要返回的文档数值

  • 例如我们只想返回2条文档:
> db.hotspots.find({}, {}, 2)
  • 返回5条只含有"title"这个键的文档:
> db.hotspots.find({}, { "title": 1, "_id": 0 }, 5)

除了在第三个参数传入limit,我们也可以在find后使用limit函数

> db.hotspots.find({}, { "title": 1, "_id": 0 }).limit(5)

上面的两条命令返回值会完全一致,只会返回5条文档。

skip

find()方法的第四个参数为skip,我们可以使用它略过指定条数的文档

> db.hotspots.find({}, { "title": 1, "_id": 0 }, 20, 20)

上面查询命令的意思是:查询20条数据,并略过前20条。也就是说查询第2页的数据,每页显示20条数据。

分页的逻辑其实很简单:

> db.collection.find({}, {}, size, (page - 1) * size)
  • size是每页显示的数据
  • page是第几页(以1为起始页的情况)

当然我们也可以在find后使用skip函数进行略过数据:

> db.hotspots.find({}, { "title": 1, "_id": 0 }, 20).skip(20)

sort

sort()方法可以用来对数据进行排序,该方法接受一个对象作为参数,对象的键指定需要排序的键名,值代表排序的方向,1代表升序,-1代表降序。

例如我们想对视频播放市场从高到底进行排序:

> db.hotspots.find({}).sort( { "duration": -1 } )

查询条件

上面我们介绍的查询都是精确匹配,以及使用正则的模糊匹配,这可以对应到前端界面的搜索框(精确搜索和模糊搜索)。但在现实的使用场景中,我们一般有更加复杂的查询条件,例如时间段查询(查询指定日期范围的数据)、取模(对用户id取模做AB测试)、AND查询(用户的行为漏斗查询)等。此时我们就要学习一下MongoDB中提供的操作符。

操作符用途示例
$lt<小于db.hotspots.find({ "duration": { "$lt": 120 } })
$lte<=小于等于db.hotspots.find({ "duration": { "$lte": 120 } })
$gt>大于db.hotspots.find({ "duration": { "$gt": 120 } })
$gte>=大于等于db.hotspots.find({ "duration": { "$gte": 120 } })
$in$nin查询一个键的多个值db.hotspots.find({ "title": { "$in": [ "《原神》- 宵宫印象曲「夏日花火谣」", "《原神》2.1前瞻直播时突然发病开始胡言乱语的屑大伟" ] } })
$or查询多个键的多个条件db.hotspots.find({ "$or": [ { "title": /原神/ }, { "desc": /原神/ } ] })
$not逻辑not运算db.hotspots.find({ "title": { "$not": /原神/ } })
$nor逻辑nor运算db.hotspots.find({ "$nor": [ { "title": /原神/ },{ "desc": /原神/ } ] })
$and逻辑and运算db.hotspots.find({ "$and": [ { "title": /原神/ },{ "desc": /原神/ } ] })
$all数组查询多个值db.hotspots.find({ "danmaku": { "$all": [ "666", "yyds" ] } },{"danmaku": 1 })
$size数组查询指定长度db.hotspots.find({ "danmaku": { "$all": [ "666", "yyds" ] } },{ "danmaku": 1 })
$where在查询中执行JavaScriptdb.hotspots.find({ "$where": function() { return this.title.length === 4 && this.duration <= 60 }})

比较操作符

$lt$lte$gt$gte是全部的比较操作符,分别对应<<=>>=。 ​

例如我们查询一下视频播放时长小于等于2分钟(120秒)的视频:

> db.hotspots.find({ "duration": { "$lte": 120 } })

视频播放数量位于5万到10万之间的视频标题

> db.hotspots.find(
	{ "stat.view": { "$gte": 50000, "$lte": 100000 } }, 
  { "title": 1, "_id": 0 }
 )

查询数据更新时间为2021-08-23日的数据:

> db.hotspots.find(
  { "update_time": { "$gte": new Date("2021-08-22T16:00:00.000"), "$lte": new Date("2021-08-23T15:59:59.000Z") } },
  { "update_time": 1, "_id": 0 }  
)

OR查询

MongoDB有两种方式进行OR查询:

  • $in$nin可以用来查询一个键的多个值
  • $or可以在多个键中查询给定的值

例如运营人员想看视频:"《原神》- 宵宫印象曲「夏日花火谣」""《原神》2.1前瞻直播时突然发病开始胡言乱语的屑大伟"的相关统计数据,我们可以这么查询:

> db.hotspots.find(
  { "title": { "$in": [ "《原神》- 宵宫印象曲「夏日花火谣」", "《原神》2.1前瞻直播时突然发病开始胡言乱语的屑大伟" ] } },
  { "_id": 0, "stat": 1 }
)

如果你想查看除了这两个视频之外的其他视频的统计数据,可以使用$nin

> db.hotspots.find(
  { "title": { "$nin": [ "《原神》- 宵宫印象曲「夏日花火谣」", "《原神》2.1前瞻直播时突然发病开始胡言乱语的屑大伟" ] } },
  { "_id": 0, "stat": 1 }
)

如果我们想查看desctitle中含有"原神"的数据,我们可以使用$or

> db.hotspots.find(
  { "$or": [ { "title": /原神/ }, { "desc": /原神/ } ] }
)

NOT查询

如果我们想查看title中不含有"原神"的数据,我们可以使用$not

> db.hotspots.find({ "title": { "$not": /原神/ } })

如果我们想查titledesc中不含有"原神"的数据,我们可以使用$nor

> db.hotspots.find(
  { "$nor": [ { "title": /原神/ },{ "desc": /原神/ } ] }
)

AND查询

如果我们想查看titledesc同时含有"原神"的数据,我们可以使用$and

> db.hotspots.find(
  { "$and": [ { "title": /原神/ },{ "desc": /原神/ } ] }
)

数组查询

因为目前的测试数据中没有数组,我们先来修改两条视频数据,将这两条视频的数据增加danmaku这个键,用来保存视频的弹幕。

> db.hotspots.update(
	{ "_id": ObjectId("6123b52a64ceb40e2917c933") }, 
  { "$set": { "danmaku": [ "666", "yyds" ] } }
)
> db.hotspots.update(
	{ "_id": ObjectId("6123b52a64ceb40e2917c935") }, 
  { "$set": { "danmaku": [ "拍得太好了", "yyds", "666" ] } }
)
  1. $all

当我们要通过多个元素来匹配数组时,就需要使用$all,下面这条查询指令会匹配到一条数据

> db.hotspots.find(
  { "danmaku": { "$all": [ "666", "拍得太好了" ] } },
  { "danmaku": 1 }
)

下面这条指令会匹配到我们所更改的两条数据,因为这两条数据的danmaku中都含有"666""yyds"

> db.hotspots.find(
  { "danmaku": { "$all": [ "666", "yyds" ] } },
  { "danmaku": 1 }
)

如果这里不使用$all,而是直接使用[ "666", "yyds" ]进行查询,这个时候就是精匹配,此时一条数据都查询不到

> db.hotspots.find(
  { "danmaku":  [ "666", "拍得太好了" ]  },
  { "danmaku": 1 }
)
  1. $size

$size对于查询数组也很有意义,例如我们想要查询数组长度为3的数组

> db.hotspots.find(
  { "danmaku":  { "$size": 3 } },
  { "danmaku": 1 }
)
  1. $slice

$slice是用下find()方法的第2个参数中,你应该还记得第2个参数是用来指定需要返回的键。假设我们中只需要返回前2条评论,

> db.hotspots.find(
  { "_id": ObjectId("6123b52a64ceb40e2917c935") }, 
  { "danmaku": { "$slice": 2 } }
)

如果你只想返回后面2条,可以将其设置为负数:

> db.hotspots.find(
  { "_id": ObjectId("6123b52a64ceb40e2917c935") }, 
  { "danmaku": { "$slice": -2 } }
)

需要注意的是,使用$slice会默认返回所有的键。

WHERE查询

有时候可能你会键/值对的查询方式不能满足你的查询需求,你可能需要结合JavaScript才能够实现你的查询需求,这个时候你可以求助于$where能够帮到你。

例如你想查询title的字符串为4个,且duration小于60s的视频:

db.hotspots.find({ "$where": function() {
  return this.title.length === 4 && this.duration <= 60
}})

$where的值是一个函数,该函数如果返回true,这文档就会被作为结果返回,如果为false就不返回。

我相信在执行这条查询指令的时候,已经对它的查询效率低下有所感受了。 $where要比常规的查询要慢上很多。因为每个BSON都需要转换成JavaScript对象,然后再运行$where的函数。如果不是走投无路,请不要考虑使用$where

Mongoose-查询

学会了Mongo查询的语法规则后,再来学习Mongoose的查询就很简单了,因为查询条件都语法都是一模一样的。在Mongoose中,Model上提供了以下几个查询的方法:

  • Model.findById()
  • Model.findOne()
  • Model.find()

我们采用上一篇教程介绍过HotSpot作为示例代码的Model

const HotSpot = mongoose.model("hotSpot", hotSpotSchema);

module.exports = HotSpot;

Model.findOne()

如果我们只想查找一条符合条件的数据,我们可以使用Model.findOne(),它的返回值是一个对象。例如我们想找到一条title!结尾的视频:

const raw = await HotSpot.findOne({ title: /!$/ });
console.log(raw);

上面的结果会返回这条视频数据的所有字段,如果你只想要title这个字段,你可以传入第二个参数,指定你需要返回的键:

const raw = await HotSpot.findOne({ title: /!$/ }, { title: 1, _id :0  })
console.log(raw)

可以看到这里的方法传参和mongosh中完全一致

Model.findById()

如果我们通过_id查找数据,那么可以使用Model.findById()方法。它等同于Model.findOne({ _id: id })

  const raw = await HotSpot.findById("6123b52a64ceb40e2917c936", {
    title: 1,
    _id: 0,
  });
  console.log(raw);

Model.find()

Model.find()应该是我们最常使用的,它的返回值是符合条件的数组,如果没有查询到任何数据,会返回一个空数组。

让我们来查询stat.view(播放数)大于1000万的视频:

 const raw = await HotSpot.find({ "stat.view": { $gt: 10000000 } });
 console.log(raw);

查询更新日期为2021-08-23日的标题数据:

const raw = await HotSpot.find(
  { "update_time": { "$gte": new Date("2021-08-22T16:00:00.000"), "$lte": new Date("2021-08-23T15:59:59.000Z") } },
  { "title": 1, "_id": 0 }  
)
console.log(raw)

分页和排序

在Mongoose中还有一个Query的构造函数,它的实例query有很多方法供我们调用。例如几个比较常用的:

  • Query.prototype.count()
  • Query.prototype.skip()
  • Query.prototype.limit()
  • Query.prototype.sort()

但是在实际使用过程中,我们不会去new Query(), 因为Model.find()本身的返回值就是一个query

const query = MyModel.find(); // `query` 是 `Query`的实例

skiplimit

我们可以在Model.find()方法传入第三个参数传入skiplimit的值来实现分页的效果。

例如我们来获取更新日期为2021-08-23日的数据,每页显示10条数据,获取第2页的数据:

const raw = await HotSpot.find(
  {
    update_time: {
      $gte: new Date("2021-08-22T16:00:00.000"),
      $lte: new Date("2021-08-23T15:59:59.000Z"),
    },
  },
  null,
  { skip: 10, limit: 10 }
);
console.log(raw);

当然我们也可以通过Query.prototype.skip()Query.prototype.limit()实现分页,下面的返回结果和上面是一致的:

const raw = await HotSpot.find(
  {
    update_time: {
      $gte: new Date("2021-08-22T16:00:00.000"),
      $lte: new Date("2021-08-23T15:59:59.000Z"),
    },
  },
  null
)
  .skip(10)
  .limit(10);
console.log(raw);

sort

同理,如果我们想对数据进行排序,也有两种写法。例如我们对播放数量超过1000万的视频,从大到小排序:

写法1:

const raw = await HotSpot.find(
  { "stat.view": { $gt: 10000000 } },
  { _id: 0, title: 1, "stat.view": 1 },
  { sort: { "stat.view": -1 } }
);
console.log(raw);

写法2:

const raw = await HotSpot.find(
  { "stat.view": { $gt: 10000000 } },
  { _id: 0, title: 1, "stat.view": 1 }
).sort({ "stat.view": -1 });
console.log(raw);

其他

参考资料

  1. Mongoose官网
  2. MongoDB官网