侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 748 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

MongoDB 文档的创建、更新和删除

zze
zze
2020-12-30 / 0 评论 / 0 点赞 / 908 阅读 / 22176 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

插入文档

插入是向 MongoDB 中添加数据的基本方法。可以使用 insert 方法向目标集合插入一个文档:

> db.foo.insert({"bar" : "baz"})

这个操作会给文档自动增加一个 _id 键(要是原来没有的话),然后将其保存到 MongoDB 中。

插入数据时, MongoDB 只对数据进行最基本的检查:检查文档的基本结构,如果没有 _id 字段,就自动增加一个。检查大小就是其中一项基本结构检查:所有文档都必须小于 16 MB。如果要查看 doc 文档的 BSON 大小(单位为字节),可以在 shell 中执行 Object.bsonsize(doc)

如果要向集合中插入多个文档,使用批量插入会快一些。使用批量插入,可以将一组文档传递给数据库。
在 shell 中,可以使用 insertMany 函数实现批量插入,它与 insert 函数非常像,只是它接受的是一个文档数组作为参数:

> db.foo.insertMany([{"_id" : 0}, {"_id" : 1}, {"_id" : 2}])
> db.foo.find()
{ "_id" : 0 }
{ "_id" : 1 }
{ "_id" : 2 }

如果在执行批量插入的过程中有一个文档插入失败,那么在这个文档之前的所有文档都会成功插入到集合中,而这个文档以及之后的所有文档全部插入失败。

> db.foo.insertMany([{"_id" : 0}, {"_id" : 1}, {"_id" : 1}, {"_id" : 2}])
uncaught exception: BulkWriteError({
	"writeErrors" : [
		{
			"index" : 2,
...
> db.foo.find()
{ "_id" : 0 }
{ "_id" : 1 }

只有前两个文档会被插入,因为插入第三个文档时会发生错误:集合中已经存在一个 _id1 的文档,不能重复插入。
在批量插入中遇到错误时,如果希望 batchInsert 忽略错误并且继续执行后续插入,可以使用 continueOnError 选项。这样就可以将上面例子中的第一个、第二个以及第四个文档都插入到集合中。 shell 并不支持这个选项,但是所有驱动程序都支持。

删除文档

现在数据库中有些数据,要删除它:

> db.foo.remove({})

上述命令会删除 foo 集合中的所有文档。但是不会删除集合本身,也不会删除集合的元信息。

remove 函数可以接受一个查询文档作为可选参数。给定这个参数以后,只有符合条件的文档才被删除。例如,假设要删除 mailing.list 集合中所有 opt-outtrue 的人:

db.mailing.list.remove({"opt-out" : true})

删除数据是永久性的,不能撤销,也不能恢复。

删除文档通常很快,但是如果要清空整个集合,那么使用 drop 直接删除集合会更快(然后在这个空集合上重建各项索引)。但也是有代价的:不能指定任何限定条件。整个集合都被删除了,所有元数据也都不见了。

更新文档

文档存入数据库以后,就可以使用 update 方法来更新它。 update 有两个参数,一个是查询文档,用于定位需要更新的目标文档;另一个是修改器(modifier)文档,用于说明要对找到的文档进行哪些修改。

更新操作是不可分割的:若是两个更新同时发生,先到达服务器的先执行,接着执行另外一个。所以,两个需要同时进行的更新会迅速接连完成,此过程不会破坏文档:最新的更新会取得“胜利”。

文档替换

例如有如下用户文档:

> db.users.insert({
    "name" : "joe",
    "friends" : 32,
    "enemies" : 2
})

我们希望将 friendsenemies 两个字段移到 relationships 子文档中。
可以在 shell 中改变文档的结构,然后使用 update 替换数据库中的当前文档:

> var joe = db.users.findOne({"name" : "joe"})
> joe.relationships = {
    "friends": joe.friends,
    "enemies": joe.enemies
}
> joe.username = joe.name
> delete joe.friends 
> delete joe.enemies 
> delete joe.name
> db.users.update({"name" : "joe"}, joe)

现在,用 findOne 查看更新后的文档结构。

> db.users.findOne({"username" : "joe"})
{
	"_id" : ObjectId("5fec26e9fd315c08c0064fe7"),
	"relationships" : {
		"friends" : 32,
		"enemies" : 2
	},
	"username" : "joe"
}

使用修改器

通常文档只会有一部分要更新。可以使用原子性的更新修改器(update modifier),指定对文档中的某些字段进行更新。更新修改器是种特殊的键,用来指定复杂的更新操作,比如修改、增加或者删除键,还可能是操作数组或者内嵌文档。

假设要在一个集合中放置网站的分析数据,只要有人访问页面,就增加计数器。可以使用更新修改器原子性地完成这个增加。每个 URL 及对应的访问次数都以如下方式存储在文档中:

> db.websys.insert({
    "url" : "www.zze.xyz",
    "pageviews" : 52
})

每次有人访问页面,就通过 URL 找到该页面, 并用 $inc 修改器增加 pageviews 的值。

> db.websys.update({"url" : "www.zze.xyz"}, {"$inc" : {"pageviews" : 1}})

现在,执行一个 find 操作,会发现 pageviews 的值增加了 1。

> db.websys.find()
{ "_id" : ObjectId("5fec28c543268dc60704420d"), "url" : "www.zze.xyz", "pageviews" : 53 }

$set:设置字段值

$set 用来指定一个字段的值。如果这个字段不存在,则创建它。例如,用户资料存储在下面这样的文档里:

> db.users.insert({
    "name" : "zze",
    "age" : 23,
    "sex" : "male",
    "location" : "Wisconsin"
})

非常简要的一段用户信息。要想添加一个字段存储喜欢的书籍,可以使用 $set

> var user = db.users.findOne({"name" : "zze"})
> db.users.update({"_id" : user._id}, {"$set" : {"favorite book" : "War and Peace"}})

之后文档就有了 favorite book 键。

> db.users.find()
{ "_id" : ObjectId("5fec29ec43268dc60704420e"), "name" : "zze", "age" : 23, "sex" : "male", "location" : "Wisconsin", "favorite book" : "War and Peace" }

如果用户突然发现自己其实不爱读书,可以用 $unset 将这个键完全删除:

> db.users.update({"_id" : user._id}, {"$unset" : {"favorite book" : 1}})

现在这个文档就和刚开始时一样了。

$inc:增加或减少值

$inc 修改器用来增加已有键的值,或者该键不存在那就创建一个。

比如有如下用户文档:

> db.users.insert({
	"name" : "zze",
	"age" : 23
})

现在马上要到 2021 年了,需要将用户的 age 字段值加 1,可以做如下更新操作:

> db.users.update({"name" : "zze"}, {"$inc" : {"age" : 1}})

更新后,可以看到:

> db.users.findOne()
{ "_id" : ObjectId("5fec2dcf43268dc607044210"), "name" : "zze", "age" : 24 }

$inc$set 的用法类似,就是专门来增加(和减少)数字的。 $inc 只能用于整型、长整型或双精度浮点型的值。要是用在其他类型的数据上就会导致操作
失败,例如 null、布尔类型以及数字构成的字符串,而在其他很多语言中,这些类型都会自动转换为数值类型。

$push:向数组追加元素

$push 会向已有的数组末尾加入一个元素,要是没有就创建一个新的数组。

例如,假设要存储博客文章,要添加一个用于保存数组的 comments(评论)键。可以向还不存在的 comments 数组添加一条评论,这个数组会被自动创建,并加入一条评论:

> db.posts.insert({
    "title" : "How to learn MongoDB???",
    "content" : "xxx",
    "author" : "zze",
})
> db.posts.update({"title" : "How to learn MongoDB???",
}, {
    "$push" : {
        "comments" : {
            "name" : "joe",
            "email" : "joe@qq.com",
            "content" : "nice post~"
        }
    }
})
> db.posts.findOne()
{
	"_id" : ObjectId("5fec2f36dc2e14872b08dd3f"),
	"title" : "How to learn MongoDB???",
	"content" : "xxx",
	"author" : "zze",
	"comments" : [
		{
			"name" : "joe",
			"email" : "joe@qq.com",
			"content" : "nice post~"
		}
	]
}

$each:向数组批量追加元素

使用 $each 子操作符,可以通过一次 $push 操作添加多个值。

比如要为一个用户文档添加一个数组字段存储用户的幸运数字,可以通过如下方式实现:

> db.users.insert({
	"name" : "zze",
	"age" : 23
})
> db.users.update({"name" : "zze"},{
    "$push" : {"luckynums" : {"$each" : [23,56,67]}}
})
> db.users.findOne()
{
	"_id" : ObjectId("5fec30ffdc2e14872b08dd41"),
	"name" : "zze",
	"age" : 23,
	"luckynums" : [
		23,
		56,
		67
	]
}

这样就可以将三个新元素添加到数组中。如果指定的数组中只含有一个元素,那这个操作就等同于没有使用 $each 的普通 $push 操作。

$slice:限制数组长度

如果希望数组的最大长度是固定的,那么可以将 $slice$push 组合在一起使用,这样就可以保证数组不会超出设定好的最大长度,这实际上就得到了一个最
多包含 N 个元素的数组:

> db.users.insert({
	"name" : "zze",
	"age" : 23
})
> db.users.update({"name" : "zze"}, {
    "$push" : {"top3" : {
        "$each" : [2,4,5,6],
        "$slice" : -3
    }}
})
> db.users.find()
{ "_id" : ObjectId("5fec3225dc2e14872b08dd42"), "name" : "zze", "age" : 23, "top3" : [ 4, 5, 6 ] }

这个例子会限制数组只包含最后加入的 3 个元素。 $slice 的值必须是负整数。

如果数组的元素数量小于等于 3($push 之后),那么所有元素都会保留。如果数组的元素数量大于 3,那么只有最后 3 个元素会保留。因此, $slice 可以用来在文档中创建一个队列。

$sort:按指定元素排序

有如下表示班级信息的一个文档,该文档中用 top3 字段保存该班级的前三名(按分数从高到底排序),可通过 $sort 实现:

> db.classes.insert({
	"name" : "c1",
})
> db.classes.update({"name" : "c1"}, {
    "$push" : {"top3" : {
        "$each" : [{"name" : "zze","score" : 88}, {"name" : "bob","score" : 12}, {"name" : "alex","score" : 44}, {"name" : "smith","score" : 23}],
        "$slice" : -3,
        "$sort" : {"score" : 1}
    }}
})
> db.classes.find()
{ "_id" : ObjectId("5fec353edc2e14872b08dd47"), "name" : "c1", "top3" : [ { "name" : "smith", "score" : 23 }, { "name" : "alex", "score" : 44 }, { "name" : "zze", "score" : 88 } ] }

$sort 引用的字段对应值为 1 表示从高到低排序,为 -1 则表示从低到高排序。

$ne:不存在则执行

如果希望用户文档中的幸运数字数组中的数字不会重复,则可以使用 $ne 实现:

> db.users.insert({
	"name" : "zze",
	"age" : 23
})
> db.users.update({"name" : "zze"},{
    "$push" : {"luckynums" : {"$each" : [23,56,67]}}
})
> db.users.update({"luckynums": {"$ne" : 56}}, {
    "$push" : {"luckynums" : 56}
})

上述示例则表示 luckynums 数组中不存在 56 这个数字才往其中插入 56 这个数字,所以后面的一个 update 操作没有任何效果。

$addToSet:去重集合

在完成上述 $ne 示例操作后 users 集合的信息如下:

> db.users.find()
{ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a"), "name" : "zze", "age" : 23, "luckynums" : [ 23, 56, 67 ] }

如果希望继续不重复的向 luckynums 数组插入值,也可使用 $addToSet 实现:

> db.users.update({ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a")}, {"$addToSet" : {"luckynums" : 57}})

还可配合 $each 实现批量插入:

> db.users.update({ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a")}, {"$addToSet" : {"luckynums" : {"$each" : [57,58,59]}}})
> db.users.find()
{ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a"), "name" : "zze", "age" : 23, "luckynums" : [ 23, 56, 67, 57, 58, 59 ] }

$pop:根据索引删除元素

若是把数组看成队列或者栈,可以用 $pop,这个修改器可以从数组任何一端删除元素。 {"$pop":{"key":1}} 从数组末尾删除一个元素,{"$pop":{"key":-1}} 则从头部删除。

> db.users.update({}, {"$pop" : {"luckynums" : -1}})
> db.users.find()
{ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a"), "name" : "zze", "age" : 23, "luckynums" : [ 56, 67, 57, 58, 59 ] }

$pull:删除指定元素

有时需要基于特定条件来删除元素,而不仅仅是依据元素位置,这时可以使用 $pull

> db.users.update({}, {"$pull" : {"luckynums" : 58}})
> db.users.find()
{ "_id" : ObjectId("5fec38dcdc2e14872b08dd4a"), "name" : "zze", "age" : 23, "luckynums" : [ 56, 67, 57, 59 ] }

$pull 会将所有匹配的文档删除,而不是只删除一个。对数组 [1,1,2,1] 执行 pull 1,结果得到 只有一个元素的数组 [2]

$setOnInsert:新增时赋值

有时,需要在创建文档的同时创建字段并为它赋值,但是在之后的所有更新操作中,这个字段的值都不再改变。这就是 $setOnInsert 的作用。 $setOnInsert 只会在文档插入时设置字段的值。因此,实际使用中可以这么做:

> db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true)
> db.users.findOne()
{
	"_id" : ObjectId("5fec444e2ef75e7441cae38d"),
	"createdAt" : ISODate("2020-12-30T09:11:42.132Z")
}

如果再次运行这个更新,会匹配到这个已存在的文档,所以不会再插入文档,因此 createdAt 字段的值也不会改变:

> db.users.update({}, {"$setOnInsert" : {"createdAt" : new Date()}}, true)
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 0 })

注意,通常不需要保留 createdAt 这样的字段,因为 ObjectId 里包含了一个用于标明文档创建时间的时间戳。但是,在预置或者初始化计数器时,或者是对于
不使用 ObjectId 的集合来说,$setOnInsert 是非常有用的。

基于位置的数组修改器

若是数组有多个值,而我们只想对其中的一部分进行操作,就需要一些技巧。有两种方法操作数组中的值:通过位置或者定位操作符($)。数组下标都是以 0 开头的,可以将下标直接作为键来选择元素。例如,这里有个文档,其中包含由内嵌文档组成的数组,比如包含评论的博客文章。

> db.blog.posts.insert({
    "content" : "xxx",
    "comments" : [
        {
            "comment" : "good post",
        	"author" : "John",
            "votes" : 0
        },
    	{
            "comment" : "i thought it too short",
        	"author" : "Claire",
            "votes" : 3
        },
        {
            "comment" : "free watches",
        	"author" : "Alice",
            "votes" : -1
        }
    ]
})

如果想增加第一个评论的投票数量,可以这么做:

> db.blog.posts.update({}, {"$inc" : {"comments.0.votes" : 1}})
> db.blog.posts.findOne()
{
	"_id" : ObjectId("5fec400adc2e14872b08dd4b"),
	"content" : "xxx",
	"comments" : [
		{
			"comment" : "good post",
			"author" : "John",
			"votes" : 1
		},
		{
			"comment" : "i thought it too short",
			"author" : "Claire",
			"votes" : 3
		},
		{
			"comment" : "free watches",
			"author" : "Alice",
			"votes" : -1
		}
	]
}

但是很多情况下,不预先查询文档就不能知道要修改的数组的下标。为了克服这个困难, MongoDB 提供了定位操作符 $,用来定位查询文档已经匹配的数组元素,并进行更新。例如,要是用户 John 把名字改成了 Jim,就可以用定位符替换他在评论中的名字:

> db.blog.posts.update({"comments.author" : "John"}, {
    "$set" : {
        "comments.$.author" : "Jim"
    }
})

定位符只更新第一个匹配的元素。所以,如果 John 发表了多条评论,那么他的名字只在第一条评论中改变。

upsert

upsert 是一种特殊的更新。要是没有找到符合更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档。如果找到了匹配的文档,则正常更新。
upsert 非常方便,不必预置集合,同一套代码既可以用于创建文档又可以用于更新文档。

我们回过头看看那个记录网站页面访问次数的例子。要是没有 upsert,就得试着查询 URL,没有找到就得新建一个文档,找到的话就增加访问次数。要是把这个写成 JavaScript 程序,会是下面这样的:

blog = db.websys.findOne({url : "/blog"})
if (blog) {
   	blog.pageviews++
   	db.websys.save(blog)
} else {
    db.websys.save({url : "/blog", pageviews : 1})
}

这就是说如果有人访问页面,我们得先对数据库进行查询,然后选择更新或者插入。要是多个进程同时运行这段代码,还会遇到同时对给定 URL 插入多个文档这样的竞态条件。

要是使用 upsert,既可以避免竞态问题,又可以缩减代码量(update 的第 3 个参数表示这是个 upsert):

> db.websys.update({"url" : "www.zze.xyz"}, {"$inc" : {"pageviews" : 1}}, true)

这行代码和之前的代码作用完全一样,但它更高效,并且是原子性的 ! 创建新文档会将条件文档作为基础,然后对它应用修改器文档。

save

save 是一个 shell 函数,如果文档不存在,它会自动创建文档;如果文档存在,它就更新这个文档。它只有一个参数:文档。要是这个文档含有 _id 键, save 会调用 upsert。否则,会调用 insert。如果在 Shell 中使用这个函数,就可以非常方便地对文档进行快速修改。

> var x = db.foo.findOne()
> x.num = 42
42
> db.foo.save(x)
> db.foo.find()
{ "_id" : ObjectId("5fec25d8fd315c08c0064fe6"), "name" : "ls", "num" : 42 }

更新多个文档

默认情况下,更新只能对符合匹配条件的第一个文档执行操作。要是有多个文档符合条件,只有第一个文档会被更新,其他文档不会发生变化。要更新所有匹配的文档,可以将 update 的第 4 个参数设置为 true

多文档更新对模式迁移非常有用,还可以在对特定用户发布新功能时使用。例如,要送给在个指定日期过生日的所有用户一份礼物,就可以使用多文档更新,将
gift 增加到他们的账号:

> db.users.update({"birthday" : "10/13/1978"}, {
    "$set" : {"gift" : "Happy Birthday!"}
}, false, true)

这样就给生日为 1978 年 10 月 13 日的所有用户文档添加了 gift 键。

想要知道多文档更新到底更新了多少文档,可以运行 getLastError 命令(可以理解为“返回最后一次操作的相关信息”)。键 n 的值就是被更新文档的数量。

> db.runCommand({getLastError : 1})

返回被更新文档

调用 getLastError 仅能获得关于更新的有限信息,并不能返回被更新的文档。可以通过 findAndModify 命令得到被更新的文档。这对于操作队列以及执行其他需要进行原子性取值和赋值的操作来说,十分方便。

假设我们有一个集合,其中包含以一定顺序运行的进程。其中每个进程都用如下形式的文档表示:

{
"_id" : ObjectId(),
"status" : state,
"priority" : N
}

status 是一个字符串,它的值可以是 READYRUNNINGDONE。需要找到状态为 READY 具有最高优先级的任务,运行相应的进程函数,然后将其状态
更新为 DONE。也可能需要查询已经就绪的进程,按照优先级排序,然后将优先级最高的进程的状态更新为 RUNNING。完成了以后,就把状态改为 DONE。就
像下面这样:

var cursor = db.processes.find({"status" : "READY"});
ps = cursor.sort({"priority" : -1}).limit(1).next();
db.processes.update({"_id" : ps._id}, {"$set" : {"status" : "RUNNING"}});
do_something(ps);
db.processes.update({"_id" : ps._id}, {"$set" : {"status" : "DONE"}});

这个算法不是很好,可能会导致竞态条件。假设有两个线程正在运行。 A 线程读取了文档, B 线程在 A 将文档状态改为 RUNNING 之前也读取了同一个文档,这样
两个线程会运行相同的处理过程。

遇到类似这样的情况时,findAndModify 就可大显身手了。 findAndModify 能够在一个操作中返回匹配结果并且进行更新。在本例中,处理过程如下所示:

> ps = db.runCommand({
    "findAndModify" : "processes",
    "query" : {"status" : "READY"},
    "sort" : {"priority" : -1},
    "update" : {"$set" : {"status" : "RUNNING"}})
{
    "ok" : 1,
    "value" : {
        "_id" : ObjectId("4b3e7a18005cab32be6291f7"),
        "priority" : 1,
        "status" : "READY"
        }
}

注意,返回文档中的状态仍然为 READY,因为 findAndModify 返回的是修改之前的文档。要是再在集合上进行一次查询,会发现这个文档的 status 已经更新
成了 RUNNING

> db.processes.findOne({"_id" : ps.value._id})
{
"_id" : ObjectId("4b3e7a18005cab32be6291f7"),
"priority" : 1,
"status" : "RUNNING"
}

这样的话,程序就变成了下面这样 :

ps = db.runCommand({"findAndModify" : "processes",
    "query" : {"status" : "READY"},
    "sort" : {"priority" : -1},
    "update" : {"$set" : {"status" : "RUNNING"}}}).value
do_something(ps)
db.process.update({"_id" : ps._id}, {"$set" : {"status" : "DONE"}})

findAndModify 可以使用 update 键也可以使用 remove 键。remove 键表示将匹配的文档从集合里面删除。例如,现在不用更新状态了,而是直接删掉,就
可以像下面这样:

ps = db.runCommand({"findAndModify" : "processes",
    "query" : {"status" : "READY"},
    "sort" : {"priority" : -1},
    "remove" : true}).value
do_something(ps)

findAndModify 命令有很多可以使用的字段:

  • findAndModify:字符串,集合名。
  • query:查询文档,用于检索文档的条件。
  • sort:排序结果的条件。
  • update:修改器文档,用于对匹配的文档进行更新(updateremove 必须指定一个)。
  • remove:布尔类型,表示是否删除文档(removeupdate 必须指定一个)。
  • new:布尔类型,表示返回更新前的文档还是更新后的文档。默认是更新前的文档。
  • fields:文档中需要返回的字段(可选)。
  • upsert:布尔类型,值为 true 时表示这是一个 upsert。默认为 false

updateremove 必须有一个,也只能有一个。要是没有匹配的文档,这个命令会返回一个错误。

0

评论区