mongoDB入门篇-文档操作之增删改

上篇我们快速了解了MongoDB的背景,以及MongoDB中的常见基本概念,并从多系统的角度进行了安装配置操作,本篇我们来学习MongoDB基础组件--文档相关的增删改基础操作

插入文档

插入

插入文档是MongoDB向集合中添加数据的基本方法,在MongoDB中可以使用insert命令来实现文档数据的添加,例如:

db.test.insert({"bar":"baz"});

即可成功将一条文档数据添加到test数据库实例中,而返回的内容:

WriteResult({ "nInserted" : 1 })

可以通过WriteResult看到刚刚触发的操作属于插入操作,而插入的行数则通过nInserted的值可以表现出来,我们来简单查询一下刚刚插入的数据:

db.test.find();
//输出
{ "_id" : ObjectId("5f208a4d0b0a1b694caff0dc"), "bar" : "baz" }

可以看到,除了我们刚刚设置的文档内容以外,mongoDB的数据中多了一个_id字段,需要注意的是在MongoDB中,如果插入的文档数据没有设置_id字段,MongoDB则会默认生成一个唯一不重复的值

批量插入

在MongoDB中同样支持多条插入,需要注意的是,MongoDB在2.X版本以后做了不少改动升级,例如在2.3版本的MongoDB中,我们可以使用batchInsert函数来完成批量插入:

db.test.batchInsert({"bar":"baz1"},{"bar":"bar2"});

但是在MongoDB3.X开始的版本中,已经将batchInsert内置函数移除,因此当我们使用同样的操作进行批量插入的时候,会遇到如下的错误:

 [js] uncaught exception: TypeError: db.test.batchInsert is not a function :

那么我们应该如何实现批量插入功能呢?从MongoDB3.1版本开始,加入了多个内置函数,其中最常用的两个:insertOneinsertMany则分别代表了插入一条和批量插入功能,如下:

//insertOne-->插入单条
db.test.insertOne({"bar":"baz1"});
//insertOne-->插入多条的时候只会将第一条插入
db.test.insertOne({"bar":"baz1"},{"bar":"baz2"});
//insertMany-->批量插入
db.test.insertMany([{"bar":"baz2"},{"bar":"baz3"}]);

除此之外,insert函数在3.1版本开始,也允许同时传入多条文档进行操作:

 db.test.insert([{"bar":"baz4"},{"bar":"baz5"}]);
//输出
BulkWriteResult({
        "writeErrors" : [ ],
        "writeConcernErrors" : [ ],
        "nInserted" : 2,
        "nUpserted" : 0,
        "nMatched" : 0,
        "nModified" : 0,
        "nRemoved" : 0,
        "upserted" : [ ]
})

可以看到操作的类型为BulkWriteResult,即批量写入结果,而nInserted对应的值也可以看出来刚才的批量插入操作完成了

注意

MongoDB我们知道,一次请求能支持的最大内存为48MB大小,因此我们使用批量操作文档的时候需要注意,尽量控制批量插入的数量,同时也因为此机制的原因,MongoDB在遇到超过48MB大小的请求后,会将数据拆分为多个48MB大小的请求处理,同时MongoDB在批量插入的过程中,如果遇到了插入失败的情况,那么在失败的数据之前的文档全部插入成功,而从失败的这条开始,后面的所有文档都会插入失败。如果希望在遇到批量插入失败的时候,依然不影响后续文档的插入,可以添加continueOnError选项,几乎主流MongoDB驱动都支持这个属性的设置。

删除文档

现在数据库中有一部分文档是我们不想要的,我们想要删除的话,可以使用如下命令:

db.test.remove()

但是需要注意的是,在mongo2.x版本中直接输入此命令是可以的,会将test集合中的所有数据都删除,但是在3.x版本开始,使用当前命令删除集合内数据将会遇到如下的错误:

[js] uncaught exception: Error: remove needs a query :
DBCollection.prototype._parseRemove@src/mongo/shell/collection.js:357:15
DBCollection.prototype.remove@src/mongo/shell/collection.js:384:18
@(shell):1:1

通过文档和该函数的源码可以看出来,在3.X版本开始,使用remove函数删除集合数据,必须有指定的条件,因此如果我们想要完成上述操作,可以改为:

db.test.remove({})

输出的结果为:

WriteResult({ "nRemoved" : 7 })

可以看到,当我们指定的条件为空时,会将整个test中的数据全部删除,需要注意的是当我们再次查看db列表的时候,可以看到,test依然存在,因为remove函数并不会删除集合本身,只会删除集合内部的数据:

show dbs;
admin   0.000GB
config  0.000GB
local   0.000GB
test    0.000GB

除此之外,如果我们真的需要删除集合的全部数据,一般不建议使用remove函数,而是改为使用drop函数删除整个集合效率会更高,我们来测试插入100w条测试数据,分别使用两个函数来删除:

for(var i = 0;i<1000000;i ++){db.test.insert({name:'test'})}
//这里我们创建了test1集合,也插入了100w测试数据
use test1;
for(var i = 0;i<1000000;i ++){db.test1.insert({name:'test'})}

我们首先使用定义一个remove函数来删除的函数,在结束的时候输出时间:

var timeFun = function(){var start = (new Date()).getTime();db.test.remove({});var timeOff = (new Date()).getTime();print('time:'+(timeOff - start));}
//调用函数
timeFun();
//输出:39614526

接着我们同样定义一个drop删除集合的函数,结束的时候,输出时间:

var timeDropFun = function(){var start = (new Date()).getTime();db.test.drop();var timeOff = (new Date()).getTime();print('time:'+(timeOff - start));}
//调用函数
timeDropFun();
//输出:5

可以看到,只用了不到10ms就完成了该操作,究其原因是因为remove函数在删除的时候需要找到对应文档所在的位置,进行删除,而drop函数则是直接对db进行删除,不需要管文档集合,当db删除以后,文档也跟着会被删除清空,因此效率上差距如此巨大

根据条件删除

如果我们需要根据一部分条件删除部分文档,只需要在remove函数中指定对应的条件即可:

db.test.remove({"name":"test"});

可以看到输出,即成功删除了2条文档数据:

WriteResult({ "nRemoved" : 2 })

更新文档

在文档插入以后,我们需要进行文档的修改操作,可以使用update函数来完成,update函数有两个参数,第一个参数是查询文档,用来指定修改的文档范围,第二个参数是修改器文档,用于说明对该范围内的文档进行怎样的修改操作。需要注意的是,更新文档的操作是不可分割的独立操作,如果同时有两个更新文档的操作发生,先到达服务器的优先执行,接着才会执行下一个更新文档的操作,同样如果有相同范围的文档的更新操作,则最终会按照最新的更新为主。

文档替换

正常情况下,我们只是需要将某部分文档的一部分进行修改,这种场景下,我们可以选择直接使用新的文档来替换旧文档的操作来完成更新,会更为有效。例如,我们需要对下面的文档进行结构上的调整,将friends和enemies字段放入子集合中:

{
        "_id" : ObjectId("5f39824e84818e5e9e7ad4a5"),
        "name" : "test",
        "friends" : 2,
        "enemies" : 2
}
 db.test.update({"_id": ObjectId("5f39824e84818e5e9e7ad4a5")},{"_id" : ObjectId("5f39824e84818e5e9e7ad4a5"),"name" : "test", "child":{"friends":2,"enemies":2}})

再次查看该文档,可以看到文档已经更新完成:

{
        "_id" : ObjectId("5f39824e84818e5e9e7ad4a5"),
        "name" : "test",
        "child" : {
                "friends" : 2,
                "enemies" : 2
        }
}

但是我们需要注意的是,如果查询条件查询到了多个文档,但是我们在进行更新的时候,没有选择使用_id参数作为查询条件,有可能会导致,更新文档中传入的__id和现有文档冲突,例如:

db.test.update({"name":"test"},{"_id" : ObjectId("5f39824e84818e5e9e7ad4a5"),"name" : "test", "child":{"friends":2,"enemies":2}});
E11001 duplicate key on update

可以看到mongo给我们抛出了E11001 duplicate key on update,究其原因,因为update操作的时候,mongo会先去查找对应条件的文档,接着会尝试使用修改器文档进行替换找到的文档,但是mongo发现在集合中,已经存在了相同的_id的文档,因此更新就会因为key冲突而更新失败,因此我们推荐在使用替换文档操作的时候,可以选择使用—id作为条件来更新

修改器更新

一般情况下,我们的文档只有一部分字段需要进行更新操作,这个时候我们可以选择使用原子性的更新修改器来完成文档的变更操作。更新修改器是一种特殊的键,可以指定复杂的更新操作,比如修改、增加或者删除键,例如,如果我们统计某个网页被访问的次数,就可以存储网页的url以及访问的次数:

{
        "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"),
        "url" : "www.baidu.com",
        "count" : 1
}

而每一次有人来访问该网站的时候,我们都要将其count字段+1,这个时候我们可以选择使用find命令读取当前数据,然后进行计算后再次替换文档的方式完成次数的统计,但是很明显,我们浪费了大量的资源完成了一个很简单的字段更新功能,并且我们在计算并替换文档的过程中,可能存在并发安全的问题,因此我们可以选择使用$inc修改器指令完成count字段的增加:

 db.set.update({"url":"www.baidu.com"},{"$inc":{"count":1}})

接着我们再次find查询当前数据,可以看到,已经成功的完成了count字段的原子递增:

 db.set.findOne();
//输出
{
        "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"),
        "url" : "www.baidu.com",
        "count" : 2
}

但是,我们需要注意的一点是,修改器虽然可以快捷的完成某部分属性或者文档的修改,但是当替换的字段不是整段文档的时候,_id的值是不能进行改变的,即修改部分属性的时候,除了__id以外,其他的键值都可以进行更改。

$set修改器

在事件的过程中,我们经常会遇到某个字段内容的变更,也有可能出现这个字段之前并不存在,但是本次需要更新出来的情况,除了文档替换这种简单粗暴的方式以外,我们可以选择$set修改器完成此操作,以刚才的案例数据为例,我们现在需要给文档中保存一下最后一次访问的时间,如下:

db.set.update({"_id":ObjectId("5f5e6a73cf72b68c1d21c471")},{"$set":{"update_time":"2020-08-13 12:00:00"}})

find查询可以看到已经更新新的键成功:

{
        "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"),
        "url" : "www.baidu.com",
        "count" : 2,
        "update_time" : "2020-08-13 12:00:00"
}

同样的,如果某些文档不需要记录update_time,我们可以使用$unset来删除掉这个属性,这样这个文档就和之前的一样了

增加/减少修改器

如果文档的一部分属性是数值类型,这个时候往往会涉及到数值的增加/减少等频繁更新的操作,而前面的案例中我们也提到了$inc修改器,该修改器就是用来增加某个键的值,需要注意的是,$inc修改器仅仅可以用于整形,长整形或者双精度浮点类型的值的变更,要是使用在其他类型,如null、布尔类型或者字符串类型上,在很多语言中,会尝试自动转型为数值来操作,除此之外,$inc操作符的值必须是数值类型,如果是非数值类型的值,则会出现'Modifier"$inc"allowed for numbers only'的错误

数组操作修改器

如果我们操作的某个属性是数组类型,比如我们期望将某个元素添加进数组中,这个时候我们就可以使用$push修改器来完成,$push修改器会向已有的数组末尾添加该元素,或者是数组不存在的话,自动创建新的数组,并且将其元素添加进数组。依然还是之前的案例,我们需要在访问网页的时候,顺便记录一下,访问的ip地址,而访问的ip是多个,我们将其存入ip_array字段中:

db.set.update({"_id":ObjectId("5f5e6a73cf72b68c1d21c471")},{"$push":{"ip_array":{"ip":"192.168.1.2"}}})

接着我们查询一下现在的set集合内容,可以看到,刚刚的文档中并没有ip_array数组,但是我们已经成功的添加进去:

{
        "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"),
        "url" : "www.baidu.com",
        "count" : 2,
        "update_time" : "2020-08-13 12:00:00",
        "ip_array" : [
                {
                        "ip" : "192.168.1.2"
                }
        ]
}

当然$push修改器每次操作仅仅能添加一个元素进入数组中,如果我们需要添加多个元素,可以考虑使用$each修改器来完成操作,此操作符可以一次push的过程中给数组中添加多个值:

db.set.update({"_id":ObjectId("5f5e6a73cf72b68c1d21c471")},{"$push":{"ip_array":{"$each":[{"ip":"192.168.1.3"},{"ip":"192.168.1.4"}]}}})

当然除此之外,我们还可以在push的时候使用$slice操作符来限制当前数组中最大长度,这样的话,在push的数量少于指定长度的时候,会全部push进去,如果超过了最大长度,则仅保留最后的指定长度的元素,这样就可以实现一个动态更新的,固定长度的队列,而在使用队列的时候,如果我们需要获取排名前十的数据,也可以选择使用$sort修改器操作符,来按照一定的字段排序。

不重复的添加修改器

有时候我们不确定是否已经存在该内容,并且不想重复插入相同内容,这个时候我们通常的做法是先find查询对应数据,判断不存在后再次进行修改操作,这种做法不仅耗时,也会存在并发操作隐患,而我们可以使用$ne修改器完成该操作,例如,我们想要判断url和当前的不一样才去修改,一样就不修改:

db.set.update({"url":{"$ne":"www.baidu.com"}},{"url":"www.baidu.com"})

但是,往往很多场景下,$ne操作符还是不够的,例如集合操作的时候,这个时候我们可以考虑使用addToSet可能更合适,例如我们在插入ip信息的时候,不希望插入同样的ip:

db.set.update({"_id":ObjectId("5f5e6a73cf72b68c1d21c471")},{"$addToSet":{"ip_array":{"ip":"192.168.1.3"}}})

这个时候我们再去查询一下:

{ "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"), "url" : "www.baidu.com", "count" : 2, "update_time" : "2020-08-13 12:00:00", "ip_array" : [ { "ip" : "192.168.1.2" }, { "ip" : "192.168.1.3" }, { "ip" : "192.168.1.4" } ] }

可以看到,原来就存在1.3的ip的情况下,的确没有再次插入新的ip,当然除此之外,$each操作符和$addToSet操作符也可以联合起来使用,完成多个元素的去重插入操作,而$ne操作符却不能联合使用

删除元素修改器

有时候我们需要从数组中删除一部分元素,如果把数组看成栈,则我们可以使用$pop修改器来完成从首端或者末端删除元素的操作,例如:

{"$pop":{"key":1}} //末端删除
{"$pop":{"key":-1}} //首端删除

如果我们想要根据条件删除数组中的元素,这个时候我们就需要使用$pull修改器完成操作,例如,我们这里将所有ip为192.168.1.2的记录都删除:

 db.set.update({},{"$pull":{"ip_array":{"ip":"192.168.1.2"}}})

接着我们再去查询一下,发现果然没有这条ip相关的记录了:

{ "_id" : ObjectId("5f5e6a73cf72b68c1d21c471"), "url" : "www.baidu.com", "count" : 2, "update_time" : "2020-08-13 12:00:00", "ip_array" : [ { "ip" : "192.168.1.3" }, { "ip" : "192.168.1.4" } ] }
定位符修改器

如果数组中有很多数据,而我们不想把所有符合条件的内容都进行操作,这个时候我们有两种操作数组值的办法,那就是基于位置下标或者使用定位操作符完成。首先明白数组都是从0开始的,我们可以直接使用下标作为键来进行操作,但是很多时候我们必须先做一次查询文档的操作,才能获取到对应的下标,因此为了解决这个问题,mongo提供了定位操作符$,用来定位我们需要操作的元素,但是定位符仅可以更新第一个匹配到的元素,如果存在多条也仅仅会修改第一条匹配的数据

upsert更新

upsert是一种特殊的更新,如果没有找到符合条件的文档数据,就会视为本次更新操作为插入文档的操作,如果找到了合适的文档,那么就按照更新的方式处理。使用upsert,可以合理的避免并发竞争情况下的更新问题,也可以减少请求和代码量,例如:

 db.set.update({"url":"www.baidu.com"},{"$inc":{"count":2}},true)

这里需要注意的是,update函数的第三个参数上我们设置了true,而第三个参数则是代表当前的update操作是否为upsert,我们设置为true以后,代表按照upsert的方式执行,并且我们需要明白的是,虽然我们查询数据判断是否存在以后,再去进行update或者save操作也是可以的,但是我们使用upsert的更新操作是属于原子性的操作!并且如果该数据不存在的话,会按照第一个参数上的条件作为基础数据,直接创建该数据,相对而言简便实用了不少

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272