说明 本文中使用的数据库是教程提供的
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;
}
基础用法
- 查询全部文档:
find()
方法的第一个参数query
用于指定查询条件,决定了要返回哪些文档。要是不指定,默认就是{}
,此时会返回集合中的所有文档。
> db.hotspots.find()
- 查询指定值:
将键值对的对象作为第一个参数传入,就可以查找指定值的文档:
- 查找指定
title
> db.hotspots.find({ "title": "酒窖?超级无敌持续战斗状态!" })
- 查找指定
aid
> db.hotspots.find({ "aid": 890051687 })
- 查询内嵌文档:
find()
方法也支持查询内嵌文档,例如我们来搜索名字为"文武俩兄弟"
的UP主:
db.hotspots.find({ "owner.name": "文武俩兄弟" })
- 正则匹配:
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 })
分页和排序
通过limit
和skip
我们可以进行分页操作,这在平时的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 | 在查询中执行JavaScript | db.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 }
)
如果我们想查看desc
或title
中含有"原神"
的数据,我们可以使用$or
:
> db.hotspots.find(
{ "$or": [ { "title": /原神/ }, { "desc": /原神/ } ] }
)
NOT查询
如果我们想查看title
中不含有"原神"
的数据,我们可以使用$not
> db.hotspots.find({ "title": { "$not": /原神/ } })
如果我们想查title
和desc
中不含有"原神"
的数据,我们可以使用$nor
> db.hotspots.find(
{ "$nor": [ { "title": /原神/ },{ "desc": /原神/ } ] }
)
AND查询
如果我们想查看title
和desc
同时含有"原神"
的数据,我们可以使用$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" ] } }
)
$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 }
)
$size
$size
对于查询数组也很有意义,例如我们想要查询数组长度为3的数组
> db.hotspots.find(
{ "danmaku": { "$size": 3 } },
{ "danmaku": 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`的实例
skip
和limit
我们可以在Model.find()
方法传入第三个参数传入skip
和limit
的值来实现分页的效果。
例如我们来获取更新日期为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);