😁 作者简介:一名大四的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:MongoDB数据库学习
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气
🔥前言:
这是MongoDB中关于文档的更新的操作,基本元素的操作是相对简单的,复杂的是对数组的更新,这一部分可以好好理解,这是我自己整理的学习笔记,希望可以帮助到大家,欢迎大家的补充和纠正
文章目录
- 3.3更新文档
- 3.3.1 文档替换
- 3.3.2 使用更新运算符
- 3.3.2.1.“ $set"修饰符入门
- 3.3.2.2.“$unset”操作符
- 3.3.2.3.递增操作和递减操作
- 3.3.2.4.数组运算符
- 3.3.3 upsert
- 3.3.3.1 upsert的基本使用
- 3.3.3.2 $setOnInsert操作符
- 3.3.3.3 save辅助函数
- 3.3.4 更新多个文档
- 3.3.5 返回被更新的文档
3.3更新文档
3.3.1 文档替换
概念:replaceOne会用新文档完全替换匹配的文档
案例:
{
"_id" : ObjectId("4b2b9f67a1f631733d917a7a"),
"name" : "joe",
"friends" : 32,
"enemies" : 2
}
🤔问题:我们希望把friends
和enemies
两个字段移到relationships
子文档中。
🌼思路:可以在shell 中更改文档的结构,然后使用 replaceOne 替换数据库中的当前文档
> var joe = db.users.findOne({"name" : "joe"});
> joe.relationships = {"friends" : joe.friends, "enemies" : joe.enemies};
{
"friends" : 32,
"enemies" : 2
}
> joe.username = joe.name;
"joe"
> delete joe.friends;
true
> delete joe.enemies;
true
> delete joe.name;
true
> db.users.replaceOne({"name" : "joe"}, joe);
现在文档如下:
{
"_id" : ObjectId("4b2b9f67a1f631733d917a7a"),
"username" : "joe",
"relationships" : {
"friends" : 32,
"enemies" : 2
}
}
❗注意点:
- 一个常见的错误是查询条件匹配到多个文档,然后更新时由第二个参数产生重复的" _id "值,数据库会抛出错误,任何文档都不会被更新
> db.people.find()
{"_id" : ObjectId("4b2b9f67a1f631733d917a7b"), "name" : "joe", "age" : 65}
{"_id" : ObjectId("4b2b9f67a1f631733d917a7c"), "name" : "joe", "age" : 20}
{"_id" : ObjectId("4b2b9f67a1f631733d917a7d"), "name" : "joe", "age" : 49}
如果今天是第二个 joe 的生日,我们需要增加 “age” 的值,那么可能会这么做:
> joe = db.people.findOne({"name" : "joe", "age" : 20});
{
"_id" : ObjectId("4b2b9f67a1f631733d917a7c"),
"name" : "joe",
"age" : 20
}
> joe.age++;
> db.people.replaceOne({"name" : "joe"}, joe);
E11001 duplicate key on update
当执行更新操作时,数据库会搜索匹配 {“name” : “joe”} 条件的文档。第一个被找到的是 65 岁的 joe。然后数据库会尝试用变量 joe 中的内容替换找到的文档,但是在这个集合中已经有一个相同 “_id” 的文档存在,因此,更新操作会失败,因为 “_id” 必须是唯一的
总结:我们需要确保更新的始终指定一个唯一的文档,例如:使用" _id"的键来匹配
3.3.2 使用更新运算符
3.3.2.1.“ $set"修饰符入门
概念:“$set” 用来设置一个字段的值。如果这个字段不存在,则创建该字段
案例:
- 修改普通的文档:
> db.users.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin"
}
> db.users.updateOne({"_id" : ObjectId("4b253b067525f35f94b60a31")},
... {"$set" : {"favorite book" : "War and Peace"}})
> db.users.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"name" : "joe",
"age" : 30,
"sex" : "male",
"location" : "Wisconsin",
"favorite book" : "War and Peace"
}
- 修改内嵌文档:
> db.blog.posts.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe",
"email" : "joe@example.com"
}
}
> db.blog.posts.updateOne({"author.name" : "joe"},
... {"$set" : {"author.name" : "joe schmoe"}})
> db.blog.posts.findOne()
{
"_id" : ObjectId("4b253b067525f35f94b60a31"),
"title" : "A Blog Post",
"content" : "...",
"author" : {
"name" : "joe schmoe",
"email" : "joe@example.com"
}
}
3.3.2.2.“$unset”操作符
概念:可以将一个或多个字段从文档中删除,使其不再存在于文档中。
语法:$unset 的参数是一个文档,其中键是要删除的字段名,而值可以是任何非零的数字。在实际中,通常将值设置为 1 或 true 表示删除字段,而将值设置为 0 或 false 表示保留字段。
案例:
//更新前的文档
{
"_id": 1,
"name": "Alice",
"age": 30,
"email": "alice@example.com"
}
db.users.update(
{ _id: 1 },
{
$unset: {
email: 1
}
}
)
//更新后的文档
{
"_id": 1,
"name": "Alice",
"age": 30
}
3.3.2.3.递增操作和递减操作
概念:“$inc” 运算符可以用来修改已存在的键值或者在该键不存在时创建它。对于更新分析数据、因果关系、投票或者其他有数值变化的地方,使用这个会非常方便。
案例:
🤔 假设我们创建了一个关于游戏的集合,将游戏和变化的分数都存储在了里面。当用户玩弹球游戏时,我们可以插入一个包含游戏名称和玩家的文档来标识不同的游戏
> db.games.insertOne({"game" : "pinball", "user" : "joe"})
//当小球撞到砖块时,就会给玩家加分。分数可以随便给,
//这里约定玩家得分的基数为50。可以使用 "$inc" 修饰符给玩家加 50 分:
> db.games.updateOne({"game" : "pinball", "user" : "joe"},
... {"$inc" : {"score" : 50}})
//更新后的文档
> db.games.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"game" : "pinball",
"user" : "joe",
"score" : 50
}
//如果小球落入了“加分”区,要加 100 00 分。可以给 "$inc" 传递一个不同的值:
> db.games.updateOne({"game" : "pinball", "user" : "joe"},
... {"$inc" : {"score" : 10000}})
//第二次更新后的文档
> db.games.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"game" : "pinball",
"user" : "joe",
"score" : 10050
}
❗注意点:
- “ i n c " 是专门用来对数字进行递增和递减操作的。 " inc" 是专门用来对数字进行递增和递减操作的。" inc"是专门用来对数字进行递增和递减操作的。"inc” 只能用于整型、长整型或双精度浮点型的值
- “ i n c " 键的值必须为数字类型。不能使用字符串、数组或者其他非数字类型的值。否则会提示错误信息:“ M o d i f i e r " inc" 键的值必须为数字类型。不能使用字符串、数组或者其他非数字类型的值。否则会提示错误信息:“Modifier " inc"键的值必须为数字类型。不能使用字符串、数组或者其他非数字类型的值。否则会提示错误信息:“Modifier"inc” allowed for numbers only”
3.3.2.4.数组运算符
数组运算符只能用于包含数组值的键。例如,不能将元素插入一个整数中,也不能从一个字符串中弹出元素。请使用 “
s
e
t
"
或
"
set" 或 "
set"或"inc” 来修改标量值。
**1.添加元素:**如果数组已存在,“$push” 就会将元素添加到数组末尾;如果数组不存在,则会创建一个新的数组
> db.blog.posts.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "..."
}
> db.blog.posts.updateOne({"title" : "A blog post"},
... {"$push" : {"comments" :
... {"name" : "joe", "email" : "joe@example.com",
... "content" : "nice post."}}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
> db.blog.posts.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"title" : "A blog post",
"content" : "...",
"comments" : [
{
"name" : "joe",
"email" : "joe@example.com",
"content" : "nice post."
}
]
}
**2.$each运算符 **
概念:是数组更新操作符中的一个用于指定要插入或添加到数组字段的多个元素的操作符
案例:
- 使用$each在一次操作中添加3个值
> db.stock.ticker.updateOne({"_id" : "GOOG"},
... {"$push" : {"hourly" : {"$each" : [562.776, 562.790, 559.123]}}})
- 如果只允许数组增长到某个长度,则可以使用 “$slice” 修饰符配合 $push 来防止数组的增长超过某个大小,从而有效地生成“top N ”列表:
> db.movies.updateOne({"genre" : "horror"},
... {"$push" : {"top10" : {"$each" : ["Nightmare on Elm Street", "Saw"],
... "$slice" : -10}}})
- 这样会根据 “rating” 字段的值对数组中的所有对象进行排序,然后保留前 10 个
> db.movies.updateOne({"genre" : "horror"},
... {"$push" : {"top10" : {"$each" : [{"name" : "Nightmare on Elm Street",
... "rating" : 6.6},
... {"name" : "Saw", "rating" : 4.3}],
... "$slice" : -10,
... "$sort" : {"rating" : -1}}}})
3.将数组作为集合使用
我们可能希望将数组视为集合,仅当一个值不存在时才进行添加。有两种方法
- $ne操作符
**概念:**表示 “not equal”,用于匹配不等于指定值的文档
案例:
要是作者不在引文的列表中,就将其添加进去
> db.papers.updateOne({"authors cited" : {"$ne" : "Richie"}},
... {$push : {"authors cited" : "Richie"}})
- $addToSet操作符
**概念:**用于向数组字段添加元素,但只有在该元素不存在于数组中时才添加。如果数组中已经存在相同的元素,则不会重复添加
案例:
> db.users.updateOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")},
... {"$addToSet" : {"emails" : "joe@gmail.com"}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 0 }
> db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com"
]
}
> db.users.updateOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")},
... {"$addToSet" : {"emails" : "joe@hotmail.com"}})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 }
> db.users.findOne({"_id" : ObjectId("4b2d75476cc613d5ee930164")})
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"username" : "joe",
"emails" : [
"joe@example.com",
"joe@gmail.com",
"joe@yahoo.com",
"joe@hotmail.com"
]
}
4.删除元素
- 如果数组视为队列或者栈,那么这时候就是根据位置进行删除了,可以使用**KaTeX parse error: Expected '}', got 'EOF' at end of input: …符**从任意一端删除元素。{"pop" : {“key” : 1}} 会从数组末尾删除一个元素,{“$pop” : {“key” : -1}} 则会从头部删除它。
- 有时候需要根据特定条件删除元素,“$pull” 用于删除与给定条件匹配的数组元素
案例:
//1.插入元素
> db.lists.insertOne({"todo" : ["dishes", "laundry", "dry cleaning"]})
//2.删除laundry元素
> db.list.updateOne({},{$pull:{"todo":"laundry"})
//3.查看删除后的文档
> db.lists.findOne()
{
"_id" : ObjectId("4b2d75476cc613d5ee930164"),
"todo" : [
"dishes",
"dry cleaning"
]
}
“$pull” 会删除所有匹配的文档,而不仅仅是一个匹配项。如果你有一个数组 [1, 1, 2, 1]并执行 pull 1,那么你将得到只有一个元素的数组 [2]。
5.基于位置的数组更改
当数组中有多个值,但我们只想修改其中的一部分时,在操作上就需要一些技巧了。有两种方法可以操作数组中的值:
- 按位置(按数组下标)
- 使用定位运算符($字符)
🍀 我的思考:
这两种方法不是对立或者某一个方法延申的存在,更多的是互补的存在,第二种方法是补充第一种方法按位置更改数组时,如果不预先查询文档并进行检查,就不知道要修改数组的下标,所以第二种方法:提供了一个定位运算符$,它可以计算出查询文档匹配的数组元素并更新该元素
案例:
- 数组使用从0开始的下标,可以将下标当作文档的键来选择元素
假设我们有一个包含数组的文档,数组中又内嵌了一些文档,比如带有评论的博客文章
//1. 查询原文档
> db.blog.posts.findOne()
{
"_id" : ObjectId("4b329a216cc613d5ee930192"),
"content" : "...",
"comments" : [
{
"comment" : "good post",
"author" : "John",
"votes" : 0
},
{
"comment" : "i thought it was too short",
"author" : "Claire",
"votes" : 3
},
{
"comment" : "free watches",
"author" : "Alice",
"votes" : -5
},
{
"comment" : "vacation getaways",
"author" : "Lynn",
"votes" : -7
}
]
}
//2.增加第一天评论的投票数量
> db.blog.updateOne({"post" : post_id},
... {"$inc" : {"comments.0.votes" : 1}})
- 如果有一个名为John的用户将其名字改为Jim,那么可以使用定位运算符在评论中进行替换
> db.blog.updateOne({"comments.author" : "John"},
... {"$set" : {"comments.$.author" : "Jim"}})
❗注意:定位运算符只会更新第一个匹配到的元素
6.使用数组过滤器进行更新
arrayFilters。此选项使我们能够修改与特定条件匹配的数组元素
案例:
如果想隐藏所有拥有 5 个或以上反对的评论,那么可以执行以下操作:
db.blog.updateOne(
{"post" : post_id },
{ $set: { "comments.$[elem].hidden" : true } },
{
arrayFilters: [ { "elem.votes": { $lte: -5 } }]
}
)
3.3.3 upsert
3.3.3.1 upsert的基本使用
概念:upsert 是一种特殊类型的更新。如果找不到与筛选条件相匹配的文档,则会以这个条件和更新文档为基础来创建一个新文档;如果找到了匹配的文档,则进行正常的更新
案例:
有个记录每个网页访问次数的例子。如果没有 upsert,那么我们可能会尝试查找 URL 并增加访问次数,或者在 URL 不存在的情况下创建一个新文档。如果把它写成一个 JavaScript 程序,它可能看起来像下面这样:
// 检查这个页面是否有一个文档
blog = db.analytics.findOne({url : "/blog"})
// 如果有,就将访问次数加1并进行保存
if (blog) {
blog.pageviews++;
db.analytics.save(blog);
}
// 否则,为这个页面创建一个新文档
else {
db.analytics.insertOne({url : "/blog", pageviews : 1})
}
这意味着每次有人访问一个页面时,我们都要先对数据库进行查询,然后选择更新或者插入。如果在多个进程中运行这段代码,则还会遇到竞争条件,可能对一个给定的 URL 会有多个文档被插入
所以我们可以使用upsert来消除竞争条件并减少代码量(updateOne和updateMany的第三个参数是一个选项文档,使其能够对其进行指定)
> db.users.updateOne({"rep" : 25}, {"$inc" : {"rep" : 3}}, {"upsert" : true})
WriteResult({
"acknowledged" : true,
"matchedCount" : 0,
"modifiedCount" : 0,
"upsertedId" : ObjectId("5a93b07aaea1cb8780a4cf72")
})
> db.users.findOne({"_id" : ObjectId("5727b2a7223502483c7f3acd")} )
{ "_id" : ObjectId("5727b2a7223502483c7f3acd"), "rep" : 28 }
3.3.3.2 $setOnInsert操作符
概念:“$setOnInsert” 是一个运算符,它只会在插入文档时设置字段的值
案例:
> db.users.updateOne({}, {"$setOnInsert" : {"createdAt" : new Date()}},
... {"upsert" : true})
{
"acknowledged" : true,
"matchedCount" : 0,
"modifiedCount" : 0,
"upsertedId" : ObjectId("5727b4ac223502483c7f3ace")
}
> db.users.findOne()
{
"_id" : ObjectId("5727b4ac223502483c7f3ace"),
"createdAt" : ISODate("2016-05-02T20:12:28.640Z")
}
如果再次运行这个更新,就会匹配到这个已经存在的文档,所以不会再进行插入,“createdAt” 字段的值也不会被改变:
> db.users.updateOne({}, {"$setOnInsert" : {"createdAt" : new Date()}},
... {"upsert" : true})
{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 0 }
> db.users.findOne()
{
"_id" : ObjectId("5727b4ac223502483c7f3ace"),
"createdAt" : ISODate("2016-05-02T20:12:28.640Z")
}
通常不需要保留 “createdAt” 这样的字段,因为 ObjectId 中包含了文档创建时的时间戳。不过,在预置或初始化计数器时,以及对于不使用 ObjectId 的集合来说,“$setOnInsert” 是非常有用的。
3.3.3.3 save辅助函数
概念:save 是一个 shell 函数,它可以在文档不存在时插入文档,在文档存在时更新文档。它只将一个文档作为其唯一的参数,如果文档中包含 “_id” 键,save 就会执行一个 upsert。否则,将执行插入操作
3.3.4 更新多个文档
updateOne 只会更新找到的与筛选条件匹配的第一个文档。如果匹配的文档有多个,它们将不会被更新。要修改与筛选器匹配的所有文档,请使用 updateMany。updateMany 遵循与 updateOne 同样的语义并接受相同的参数。关键的区别在于可能会被更改的文档数量
案例:
假设我们想给每个在某一天过生日的用户一份礼物,则可以使用 updateMany 向他们的账户添加一个 “gift” 字段
> db.users.insertMany([
... {birthday: "10/13/1978"},
... {birthday: "10/13/1978"},
... {birthday: "10/13/1978"}])
{
"acknowledged" : true,
"insertedIds" : [
ObjectId("5727d6fc6855a935cb57a65b"),
ObjectId("5727d6fc6855a935cb57a65c"),
ObjectId("5727d6fc6855a935cb57a65d")
]
}
> db.users.updateMany({"birthday" : "10/13/1978"},
... {"$set" : {"gift" : "Happy Birthday!"}})
{ "acknowledged" : true, "matchedCount" : 3, "modifiedCount" : 3 }
3.3.5 返回被更新的文档
在某些场景中,返回修改过的文档是很重要的,在MongoDB3.2中,我们可以使用三个方法来实现返回更新后的文档
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
这些方法与updateOne之间的主要区别在于,它们可以原子获取已修改文档的值