说明 本文中使用的数据库是教程提供的
bilibili_hot数据库,可以通过前文《MongoDB和Mongoose基础》查看如何下载并导入教程提供的数据。
- MongoDB的相关操作是在mongosh进行操作
- Mongoose相关操作是在以Express.js为基础的Node.js后端服务中进行操作
MongoDB
创建文档
插入单个文档
使用db.collection.insertOne()方法可以向一个集合中插入一条文档,让我们切换到bilibili_hot,并在hotspots集合中插入一条数据:
> use bilibili_hot
switched to db bilibili_hot
> db.hotspots.insertOne({ "name": "这是一条新的数据" })
WriteResult({ "nInserted" : 1 })
你会发现这条插入的文档数据,和我们hotspots集合中已有的数据结构都不一致,已有的集合中的文档没有name这个建,且数据结构要比这条文档复杂得多。这其实是我们上篇教程里面说的,MongoDB中的集合是动态模式的, 这意味着一个集合里面的文档可以是各式各样的(但是不推荐这样做)。
我们再在一个不存在的集合中插入一条数据,来加深对动态模式的理解,我们先查看现在的集合应该只有一个hotspots:
> show collections
hotspots
然后我们再books集合中插入一条数据:
> db.books.insertOne({ name: '《MongoDB权威指南》' })
WriteResult({ "nInserted" : 1 })
查看现在的所有集合:
> show collections
books
hotspots
如果你有类似关系型数据库的经验,你可能会想到插入一条数据的前应该先创建一张表,对应到MongoDB中就是先创建一个集合,然而MongoDB中的集合是可以被动态创建的。
插入多个文档
使用db.collection.insertMany()方法可以向一个集合中插入多个文档
> db.hotspots.insertMany([{ "title": "视频1" }, { "title": "视频2" }])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("612afe521d4385df4950fd74"),
ObjectId("612afe521d4385df4950fd75")
]
}
从返回值中我们可以看到返回了一个insertedIds字段,其中数据类型是ObjectId。这是因为MongoDB为每个文档自动创建了_id字段,数据类型是ObjectId。
其他插入文档的方式
上面是两种最常用的插入文档的方式,但是MongoDB其实提供了其他可以实现插入操作的方法,这里我们不进行一一讲解。感兴趣的可以查看官网5.0文档《MongoDB Manul-Insert Methods》。
db.collection.insert():insertOne()和insertMany()的简写db.collection.update(): 需要设置{upsert: true}选项db.collection.updateOne():需要设置{upsert: true}选项db.collection.updateMany():需要设置{upsert: true}选项db.collection.findAndModify():需要设置{upsert: true}选项db.collection.findOneAndUpdate():需要设置{upsert: true}选项db.collection.findOneAndReplace():需要设置{upsert: true}选项db.collection.bulkWrite()
删除文档
db.collection.remove()方法可以删除集合中的所有文档,但不会删除集合本身。remove()函数可以接收一个查询条件作为参数,给定这个从参数后,之后符合条件的文档才会被删除。
让我们来删除title为"设 计 鬼 才"的文档:
> db.hotspots.remove({ "title": "设 计 鬼 才" })
WriteResult({ "nRemoved" : 70 })
可以看到,集合中有70个文档被删除了,因为我们的集合中有70条{ "title": "设 计 鬼 才" }的数据(这个删除数据是永久性的,不能撤销也不能恢复)
MongoDB也提供了db.collection.findOneAndDelete()方法用于删除文档,和db.collection.remove()不同的是,该方法会将被删除的那条文档返回到客户端。
让我们来删除一条最晚更新,且title为"设 计 鬼 才"的这条文档吧。首先我们看看目前更新时间最晚的数据是什么时候:
> db.hotspots.find({ "title": "设 计 鬼 才" }, { update_time: 1, _id: 0 }).sort( { update_time: -1 } ).limit(1)
{ "update_time" : ISODate("2021-08-25T02:00:00.125Z") }
现在我们使用db.collection.findOneAndDelete()来删除这条文档:
> db.hotspots.findOneAndDelete({ "title": "设 计 鬼 才" }, { sort: { update_time: -1 } })
{
"_id" : ObjectId("6125a42064ceb40e291801ac"),
"title" : "设 计 鬼 才",
...
"update_time" : ISODate("2021-08-25T02:00:00.125Z"),
"__v" : 0
}
可以看到删除并返回了更新时间最晚的那条文档。
更新文档
更新操作的内容相比较起来会相对复杂一些,MongoDB的文档中介绍了4个基本的更新方法,分别是:
db.collection.updateOne(<filter>, <update>, <options>)db.collection.updateMany(<filter>, <update>, <options>)db.collection.replaceOne(<filter>, <update>, <options>)db.collection.update(<filter>, <update>, <options>)
方法的第一个参数是查询文档,用来定位你要更新的目标文档,第二个参数是更新操作符,用于说明对于文档的更新条件,第三个参数是可选参数。
我们先来讲讲第二个参数,更新操作符的具体使用。第一个查询文档,我们会在下一个章节讲解,不用担心,目前我们只会使用比较简单的查询条件。
更新操作符
例如我们要对下面一条文档进行更改:
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
"title" : "中元节潮汕文化短片《番客》:希望每一个亡灵都有人纪念,每一个人终能落叶归根",
"update_time" : "2021-08-23T14:48:10.392Z",
"stat" : {
"aid" : 890021627,
"view" : 610680,
"danmaku" : 6934,
"reply" : 6116,
"favorite" : 31956,
"coin" : 63543,
"share" : 20852,
"now_rank" : 0,
"his_rank" : 71,
"like" : 68606,
"dislike" : 0
}
...
}
$set
我们希望将title的值变短一点,更改为"中元节潮汕文化短片《番客》",并将update_time更改为最新时间,我们可以使用updateOne()方法。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$set": { "title": "中元节潮汕文化短片《番客》", "update_time": new Date() } }
)
使用findOne()查找该文档:
> db.hotspots.findOne({ "_id": ObjectId("6123b52a64ceb40e2917c938") })
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
...
"title" : "中元节潮汕文化短片《番客》",
"update_time" : ISODate("2021-08-31T14:11:08.560Z"),
...
}
可以看到此时title字段和update_time字段都已经更新成功了。这是$set在业务中最常见的使用方式,更改文档的某个值。
目前我们使用$set的字段都是文档中已经存在的字段,如果这个字段不存在,则$set会创建它,例如我们来设置一个新的字段。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$set": { "newkey": "新的字段" } }
)
再次查找该文档,就有了newKey这个新的字段
> db.hotspots.findOne({ "_id": ObjectId("6123b52a64ceb40e2917c938") })
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
...
"title" : "中元节潮汕文化短片《番客》",
"update_time" : ISODate("2021-08-31T14:11:08.560Z"),
"newkey" : "新的字段"
...
}
$set字段还可以修改内嵌文档的值,例如文档中的stat.view表示视频的播放量,当前这个视频的播放量是610680,我们来将它变成610681。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$set": { "stat.view": 610681 } }
)
$unset
$unset可以用来删除某个字段,还记得我们刚才新增的newkey这个字段吗?我们来把它删除了吧
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$unset": { "newkey": 1 } }
)
$inc
$inc可以用来增加或者减少数字,还记得我们刚才把播放量stat.view的数值从610680变成了610681吗?实际上我们是先获取到了文档的播放量,再将播放量+1的数据设置给了stat.view,但如果使用$inc,我们就能够减少获取文档的那一步了。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$inc": { "stat.view": 1 } }
)
相信我,这个时候播放量肯定已近变成610682了。
我们再来给这个视频的up主献上两个硬币吧:
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$inc": { "stat.coin": 2 } }
)
或者这个视频热度变高了,提升了10名。则his_rank的排名需要从71减去10
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$inc": { "stat.his_rank": -10 } }
)
下面我们介绍的几个更新操作符是在数组中使用,我们来新增一个叫danmaku的字段用来保存弹幕。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$set": { "danmaku": [] } }
)
$push
我们可以使用$push,将弹幕的数据添加到danmaku字段中:
db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$push": { "danmaku": '666' } }
)
此时查找该文档,你会发现我们已经成功添加了一条弹幕数据
> db.hotspots.find({ "_id": ObjectId("6123b52a64ceb40e2917c938") })
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
"title" : "中元节潮汕文化短片《番客》",
"danmaku" : [
"666"
]
}
如果需要添加多个弹幕,我们可以使用子操作符$each
db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$push": { "danmaku": { "$each": [ "yyds", "是潮汕话!!!", "泪目了" ] } } }
)
如果你想维护一个固定长度的数组,你还可以使用子操作符$slice,$slice的值必须是个负整数,例如你设置为-10,当数组长度小于10的时候,所有的数据都会保留。如果数据大于10,此时只会保留最后进来的10条数据。
现在的danmaku中有4条弹幕
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
...
"danmaku": [
"666",
"yyds",
"是潮汕话!!!",
"泪目了"
]
}
让我们在再添加两条数据,并将danmaku的最大长度设置为5。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$push": {
"danmaku": {
"$each": [ "太真实了吧!", "这里拍得是在是太棒了!" ],
"$slice": -5
}
}
}
)
此时的danmaku中有5条弹幕,最早的一条666已经不在了。
{
"_id" : ObjectId("6123b52a64ceb40e2917c938"),
...
"danmaku": [
"yyds",
"是潮汕话!!!",
"泪目了",
"太真实了吧!",
"这里拍得是在是太棒了!"
]
}
$addToSet
你可能将数组作为Set使用,让里面的元素唯一,那么你可以使用$addToSet而不是$push。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$addToSet": { "danmaku": "太真实了吧!" } }
)
执行此条命令的返回值是如下:
{
"acknowledged" : true,
"matchedCount" : 1.0,
"modifiedCount" : 0.0
}
可以看到modifiedCount的值为0,此时没有任何值被更改,因为danmaku中已经存在此条弹幕了,除非你插入一条数组里面没有的数据:
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$addToSet": { "danmaku": "太好看了吧!" } }
)
$pop和$pull删除元素
我们可以使用$pop从数组的任意一端删除元素,{ "$pop": { "key ":1 } }从数组末尾删除,{ "$pop":{ "key ":-1 } }从头部删除。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$pop": { "danmaku": 1 } }
)
当然我们还可以使用$pull删除指定条件的元素,例如让我们删除弹幕中的所有yyds。
> db.hotspots.updateOne(
{ "_id": ObjectId("6123b52a64ceb40e2917c938") },
{ "$pull": { "danmaku": "yyds" } }
)
可选参数
下面我们来讲一下更新方法的第三个参数,它是一个对象,可以传入多个参数。
upsert
在实际业务中,我们经常会遇到在更新之前需要查找集合中是否存在这条数据的情况,假设我们有一张fans集合,专门用来存储up主的粉丝数据。当一个新人up主最开始粉丝数为0的时候,update()方法在fans这个集合中是找不到这个up主的数据的,所以先要先使用findOne()查找up主,如果没有我们就需要创建文档,如果有再执行update()。
那么有没有可以只使用一条语句完成这个操作呢?有的,那就是使用upsert。
upsert是一种特殊的更新,要是没有找到符合条件的文档,那么就会创建一个新的文档,如果有符合条件的文档,则正常更新。让我们使用upsert来完成刚才的需求,此时我们的数据库中甚至没有fans集合。
db.fans.updateOne(
{ "mid": 57938157 },
{ "$push": { "fan_list": "第一个粉丝" } },
{ "upsert": true }
)
此时再执行查找命令db.fans.find({}),你会发现已经创建好了为fans集合并且有了一条数据。
{
"_id" : ObjectId("612f7e277784e59e5d640217"),
"mid" : 57938157.0,
"fan_list" : [
"第一个粉丝"
]
}
好事成双,我们再来为这位up主添加一位粉丝吧:
db.fans.updateOne(
{ "mid": 57938157 },
{ "$push": { "fan_list": "第二个粉丝" } },
{ "upsert": true }
)
再次执行db.fans.find(),集合中依然只有一条文档数据:
{
"_id" : ObjectId("612f7e277784e59e5d640217"),
"mid" : 57938157.0,
"fan_list" : [
"第一个粉丝",
"第二个粉丝"
]
}
multi
当使用db.collection.update()方法时,默认只更新一个文档,如果需要更新多个,需要设置第三个参数中的{ "multi": true } 。
> db.hotspots.update(
{ "title": "中元节潮汕文化短片《番客》:希望每一个亡灵都有人纪念,每一个人终能落叶归根" },
{ "$set": { "title": "中元节潮汕文化短片《番客》", "update_time": new Date() } },
{ "multi": true }
)
Updated 95 existing record(s) in 88ms
执行上面的命令,可以看到我们更新了95条文档的title。
Mongoose
创建文档
创建Schema和Model
在上一篇教程中我们已经介绍过Mongoose的基本概念,首先需要创建Schema和Model。在Mongoose中,大部分CRUD操作都是Model的方法。我们将创建好的Model作为HotSpot.js文件导出,方便使用。
// HotSpot.js
const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const hotSpotSchema = new Schema({
aid: {
type: Number,
required: true,
},
videos: {
type: Number,
default: 1,
},
tid: {
type: Number,
default: 0,
},
tname: {
type: String,
require: true,
},
copyright: {
type: Number,
dafault: 1,
},
pic: {
type: String,
},
title: {
type: String,
require: true,
},
pubdate: {
type: Date,
},
ctime: {
type: Date,
require: true,
},
desc: {
type: String,
default: "",
},
state: {
type: Number,
},
duration: {
type: Number,
default: 0,
},
owner: {
mid: {
type: Number,
},
name: {
type: String,
},
face: {
type: String,
},
},
stat: {
aid: {
type: Number,
},
view: {
type: Number,
},
danmaku: {
type: Number,
},
reply: {
type: Number,
},
favorite: {
type: Number,
},
coin: {
type: Number,
},
share: {
type: Number,
},
now_rank: {
type: Number,
},
his_rank: {
type: Number,
},
like: {
type: Number,
},
dislike: {
type: Number,
},
},
dynamic: {
type: String,
},
cid: {
type: Number,
},
dimension: {
width: {
type: Number,
},
height: {
type: Number,
},
rotate: {
type: Number,
},
},
short_link: {
type: String,
},
short_link_v2: {
type: String,
},
first_frame: {
type: String,
},
bvid: {
type: String,
},
season_type: {
type: Number,
},
rcmd_reason: {
content: {
type: String,
},
corner_mark: {
type: Number,
},
},
update_time: {
type: Date,
},
});
const HotSpot = mongoose.model("hotSpot", hotSpotSchema);
module.exports = HotSpot;
插入单个文档
方法1: 使用**Model.create()**
使用Model.create()可以插入单个或多个文档,让我们在hotspots中插入一个文档:
HotSpot.create({ title: "true" }, function(err, raw) {
if (err) {
console.log(err)
return
}
console.log(raw)
})
执行这段代码,会发现有以下的报错
Error: hotSpot validation failed: aid: Path `aid` is required.
...
错误信息提示,aid应该是必传,我们在Schema中定义了这个键{ required: true },但是插入的对象中没有aid这个键值对,所以导致了报错。
new Schema({
aid: {
type: Number,
required: true,
}
...
})
因为有了Mongoose中Schema的存在, 可以规避MongoDB因动态模式可能会出现的同一个集合中,文档数据结构不一致的问题。让我们来从插入一个合规的热点视频的数据吧:
const validData = {
videos: 1,
tid: 172,
desc: "小马和老马的游戏已经做好了,但估计没人玩,大家去玩《异界事务所》吧。",
duration: 387,
aid: 377533236,
tname: "手机游戏",
copyright: 1,
pic: "http://i2.hdslb.com/bfs/archive/2d95711a493730101d977f60f641ab32b576c49d.jpg",
title: "设 计 鬼 才",
pubdate: new Date("1970-01-19T20:42:02.595Z"),
ctime: new Date("1970-01-19T20:42:02.596Z"),
state: 0,
owner: {
mid: 1577804,
name: "某幻君",
face: "http://i2.hdslb.com/bfs/face/a7eda2f97431d13f89a43b310262d1b19be83c01.jpg",
},
stat: {
aid: 377533236,
view: 251936,
danmaku: 4851,
reply: 2986,
favorite: 8022,
coin: 15046,
share: 563,
now_rank: 0,
his_rank: 0,
like: 47592,
dislike: 0,
},
dynamic: "设计鬼才",
cid: 395302606,
dimension: {
width: 1920,
height: 1080,
rotate: 0,
},
short_link: "https://b23.tv/BV19f4y1P7FL",
short_link_v2: "https://b23.tv/BV19f4y1P7FL",
first_frame:
"http://i2.hdslb.com/bfs/storyff/n210823a23kjji9eu348m3eernmihnbp_firsti.jpg",
bvid: "BV19f4y1P7FL",
season_type: 0,
rcmd_reason: {
content: "很多人点赞",
corner_mark: 0,
},
update_time: new Date("2021-08-23T14:48:10.392Z"),
};
HotSpot.create(validData, function(err, raw) {
if (err) {
console.log(err)
return
}
console.log(raw)
})
方法2:使用**document.save()**
Model构造函数的实例为文档,我们也可以使用document.save()方法插入单个文档
const hotSpotDoc = new HotSpot(validData);
hotSpotDoc.save(function (err, raw) {
if (err) {
console.log(err);
return;
}
console.log(raw);
});
插入多个文档
Model.insertMany()可以插入多个文档
HotSpot.insertMany([validData, validData], function(err, raw) {
if (err) {
console.log(err)
return err
}
console.log(raw)
})
删除文档
让我来看看如何使用Mongoose中如何删除文档吧,Mongoose提供了Model.deleteOne()方法删除一个文档。和Model.deleteMany()删除多个文档。
让我们来删除一条title为"英雄联盟10周年"的数据吧:
const raw = await HotSpot.deleteOne(
{ title: "英雄联盟10周年" },
);
console.log(raw)
// {
// deletedCount: 1
// }
Mongoose还提供了另外两个可以用于删除的方法,分别是:
Model.findOneAndDelete():查找匹配的文档将其删除,并将中找到的文档(如果有)传递给回调函数Model.findByIdAndDelete():通过文档的_id删除将其删除,是Model.findOneAndDelete({ _id: id })的缩写
让我们使用Model.findOneAndDelete()来删除一个更新时间最晚,且title依然为"英雄联盟10周年"的一条文档。首先在shell中查看一下现有的更新时间
> db.hotspots.find({ title: "英雄联盟10周年" }, {update_time: 1, _id: 0})
{ "update_time" : ISODate("2021-08-25T17:30:00.137Z") }
{ "update_time" : ISODate("2021-08-25T18:00:00.146Z") }
{ "update_time" : ISODate("2021-08-25T18:30:00.140Z") }
{ "update_time" : ISODate("2021-08-25T19:00:00.147Z") }
使用Model.findOneAndDelete()删除:
const raw = await HotSpot.findOneAndDelete(
{ title: "英雄联盟10周年" },
{ sort: { update_time: -1 }}
);
console.log(raw)
让我们再在shell中查看一下现有的更新时间:
> db.hotspots.find({ title: "英雄联盟10周年" }, {update_time: 1, _id: 0})
{ "update_time" : ISODate("2021-08-25T17:30:00.137Z") }
{ "update_time" : ISODate("2021-08-25T18:00:00.146Z") }
{ "update_time" : ISODate("2021-08-25T18:30:00.140Z") }
可以看到最晚的一条数据已经被删除了。
更新文档
Mongoose提供了三个基本的为更新文档方法,这三个方法和mongosh中的文档操作基本一致,这三个方法分别是:
Model.update()Model.updateMany()Model.updateOne()
我们将{ "title": "设 计 鬼 才" }的一条文档更新为{ "title": "设计鬼才" }。
const raw = await HotSpot.updateOne(
{ title: "设 计 鬼 才" },
{ $set: { title: "设计鬼才" } }
)
console.log(raw)
// {
// acknowledged: true,
// modifiedCount: 1,
// upsertedId: null,
// upsertedCount: 0,
// matchedCount: 1,
// }
我们也可以使用updateMany()方法把所有的{ "title": "设 计 鬼 才" }的文档都给更新了:
const raw = await HotSpot.updateMany(
{ title: "设 计 鬼 才" },
{ $set: { title: "设计鬼才", update_time: new Date() } }
)
console.log(raw)
// {
// acknowledged: true,
// modifiedCount: 69,
// upsertedId: null,
// upsertedCount: 0,
// matchedCount: 69,
// }
可以看到返回值modifiedCount: 69, 说明有69条数据都被更新了。
当然我们也可以使用update()方法进行更新,但需要注意的是,这里的Model.update()和mongosh中的db.collection.update()有所不同的是,Mongoose中的Model.update()会默认更改全部的符合匹配条件的文档,而在mongosh中,得设置{ "multi": true }才会更改全部文档。让我们来把title字段进行还原吧:
const raw = await HotSpot.update(
{ title: "设计鬼才" },
{ $set: { title: "设 计 鬼 才" , update_time: new Date() } }
)
console.log(raw)
// {
// acknowledged: true,
// modifiedCount: 70,
// upsertedId: null,
// upsertedCount: 0,
// matchedCount: 70,
// }
从返回值的结果上看,有70条文档的title都被更新会原来的名称了。
让我们将观看数量+1,硬币数量+2:
const raw = await HotSpot.update(
{ title: "设 计 鬼 才" },
{ $inc: { "stat.view": 1, "stat.coin": 2 } }
)
console.log(raw)
// {
// acknowledged: true,
// modifiedCount: 70,
// upsertedId: null,
// upsertedCount: 0,
// matchedCount: 70,
// }
因为在Mongoose这种的更新操作符操作和mongosh中是一致的,这里我们就不更多举例了,读者可以参考前文。