MongoDB创建、更新及删除文档

1.1插入并保存文档

插入是向MongoDB中添加数据的基本方法。对目标机使用insert方法,插入一个文档:

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

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

1.1.1 批量插入

如果要插入多个文档,使用批量插入会快一些。批量插入能传递一个由文档构成的数组给数据库。只有插入多个文档到一个集合的时候,这种方式才会有用,而不能批量插入一次对多个集合执行操作。要是只是导入原始数据(如从数据feed或Mysql中导入),可以使用命令行工具,如mongoimport,而不是使用批量插入,批量插入的时候对消息的长度有限制。

一次性插入多个文档,可以传递一个数组给 insert() 。

>db.post.insert([
{
   title: 'MongoDB Overview', 
   description: 'MongoDB is no sql database',
   by: 'tutorials test',
   url: 'http://www.test.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 100
},
{
   title: 'NoSQL Database', 
   description: 'NoSQL database doesn't have tables',
   by: 'tutorials test',
   url: 'http://www.test.com',
   tags: ['mongodb', 'database', 'NoSQL'],
   likes: 20, 
   comments: [    
      {
         user:'user1',
         message: 'My first comment',
         dateCreated: new Date(2013,11,10,2,35),
         like: 0 
      }
   ]
}
])

要插入文档,也可以使用  db.post.save(document)。 如果不指定_id在文档中,那么 save() 方法和 insert()方法工作一样。如果指定_id,它会替换整个数据文件,其中包含_id 指定save()方法。

1.1.2 插入:原理和作用

当执行插入的时候,使用的驱动程序会将数据转换成BSON的形式,然后将其送人数据库。数据库解析BSON,检验是否包含“_id”键并且文档不超过4MB,其他不做别的数据验证,只是简单将文档原样存入数据库。这样有好有坏,最明显的副作用就是允许无效的数据插入,好处是它能让数据库更加安全,远离注入式攻击。因为插入时并不执行代码,所以没有这块没有注入式攻击的可能。传统的注入式攻击对Mongodb来说无效。

 1.2 删除文档

删除现有数据库中的数据:

> db.users.remove()

上述命令删除users集合中所有文档。但不会删除集合本身,原有索引也会保留。remove函数可以接受一个查询文档作为参数。给定这个参数后,只有符合条件的文档才被删除。如:要删除mailing.list集合中所有“optout”为true的人:

> db.mailing.list.remove({optout:true})

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

删除文档通常会很快,但是要清除整个集合,直接删除集合(然后重建索引)会更快。

1.3 更新文档

使用update方法修改文档。update有两个参数,一个是查询文档,用来找出来要更新的文档,另一个是修改器(modifier)文档,描述对找到的文档做哪些修改。

更新是原子的:若是两个更新同时发生,先到达服务器的先执行,接着执行另一个,所以,互相有冲突的更新可以火速传递,并不会互相干扰:最后的更新会取得“胜利”。

1.3.1 文档替换

变成下面这样:

可以用update来替换文档:

常见错误就是查询条件匹配了多个文档,然后更新的时候由于第二个参数的存在就产生重复的“_id”值。数据库会报错,不做任何修改。(除了shell外,一般程序不会报错,除非用getLastError)

为了避免这种情况,最好确保更新总是指定唯一文档,如通过像“_id”这样的键来匹配。

1.3.2 使用修改器

通常文档只会有一部分要更新。利用原子的更新修改器,可以使得这种部分更新极为高效。更新修改器是种特殊的键,用来指定复杂的更新操作,如调整,增加或删除键,还可以是操作数组或者内嵌文档。

例如要记录网站访问情况,当有人访问的时候,就要增加计数器,可以使用更新修改器原子性地完成这个增加:

在php中,$表示变量前缀,在双引号中以$开头的字符串都会替换成变量,可以转义$:"\$foo"。也可以使用单引号'$foo',就不会解释变量。还可以设置php.ini文件的mongo.cmd_char,可以用=,:,?或者任何你觉得可以替代$的字符都可以。如选择~,就可以用~inc当作\$inc。

使用修改器时,“_id”的值不能改变。(注意,整个文档替换时是可以改变“_id”的。)其他键值,包括其他唯一索引的键,都是可以更改的。

1.“$set”修改器

“$set”用来指定一个键的值。如果这个键不存在,则创建它。这对更新模式或者增加用户定义键来说非常方便。

上述文档想要添加喜欢的书籍进去,可以使用“$set”:

要是用户觉得喜欢的其实是另外一本书,"$set"又能帮上忙:

"$set"修改数据类型,如用户喜欢的是一堆书,可以将“favorite”键的值变成一个数组

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

也可以用“$set”修改内嵌文档:

2.增加和减少

“$inc”修改器来增加已有键的值,或者在键不存在的时创建一个键。对于分析数据,因果关系,投票或者其他数值变化的地方,使用这个都会非常方便。

如游戏集合,如果小球撞到砖块,给玩家加分,这里基数给50,可以使用“$inc”修改器给玩家加50分:

分数键(score)原来并不存在,所以“$inc”创建了这个键,并把值设定成增加量:50。

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

“$inc”键的值必须为数字。不能使用字符串,数组或其他非数字的值。否则提示如下错误:

3.数组修改器

 数组操作,只能用在值为数组的键上。如不能对整数做push,也不能对字符串做pop。使用“$set”或“$inc”来修改标量值。

如果指定键已存在,“$push”会向已有的数组末尾加入一个元素,要是没有就会创建一个新的数组。

要想添加一条评论,可以使用“$push”:

经常有这种情况,一个值不在数组里面就把它加进去。可以在查询文档中用“$ne”来实现,如:要是作者不在引文中就添加进去:

也可以用“$addToSet”完成同样的事,要知道有些情况“$ne”根本行不通,有时候更适合用“$addToSet”。

将“$addToSet”和“$each”组合起来,可以添加多个不同的值,而用“$ne”和“$push”组合就不能实现。

有几个从数组删除元素的方法,若把数组看成队列和栈,可以用“$pop”,这个修改器可以从数组任何一端删除元素。{$pop:{key:1}}从数组末尾删除一个元素,{$pop:{key:-1}}则从头部删除。

有时候要特定条件删除元素,不仅是依据位置,“$pull”可以做到。如下:

“$pull”会将所有匹配的部分删掉。对数组[1,1,2,1]执行pull 1,得到结果就是只有一个元素的数组[2]。

4.数组的定位修改器

有两种方法操作数组中的值:通过位置或者定位操作符(“$”)。

数组都是以0开头的,可以将下标直接作为键来选择元素

如果想增加一个评论的投票数量:

很多情况下不预查文档,就不能知道要修改数组的下标,mongodb提供了定位操作符“$”,用来定位查询文档已经匹配的元素,并运行更新。

如:把John的名字改成Jim

定位符只更新第一个匹配的元素。所以,如果John不止一个评论,那么只有他的第一条评论中的名字被更改。

5.修改器速度

有的修改器运行很快,$inc能就地修改,因为不需要改变文档大小,只需要将键的值修改一下,所以快,而数组修改器可能更改了文档的大小,会慢一些,mongodb预留了些补白给文档,来适应大小变化,“$push”或者其他修改器是推荐使用的,要是“$push”成为瓶颈,可以将内嵌数组独立出来,放到单独一个集合里面。

1.3.3 upsert

upsert是一种特殊的更新。要是没符合更新条件的文档,就会以这个条件和更新文档为基础创建一个新的文档。如果找到了匹配的文档,则正常更新。(update的第三个参数表示这个upsert)

如前面的页面访问增加访问次数的例子:

> db.analytics.update({"url":"/blog"},{$inc:{visits:1}},true)

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

例如要是执行一个匹配键并增加对应键值的upsert操作,会在匹配的基础上进行增加:

先是remove清空了集合,里面没有文档了。upsert创建一个键“count”的值为25的文档,随后将这个值加3,最后得到“count”为28的文档。要是不开启upsert选项,{“count”:25}不会匹配到任何文档,也没有任何更改。

save Shell帮助程序

save是一个shell函数,可以在文档不存在时插入,存在时更新。它只有一个参数:文档。要是这个文档含有“_id”键,save会调用upsert。否则,会调用插入。

> var x = db.foo.findOne()
> x.num = 42
42
> db.foo.save(x)
//如果不用save,写法很啰嗦:
> db.foo.update({"_id":x._id}, x)

1.3.4 更新多个文档

默认情况下,更新只能对符合匹配条件的第一个文档执行操作。要是有多个文档符合条件,其余的文档就没有变化。要使所有匹配到的文档都得到更新,可以设置update的第4个参数为true。

如:给所有在特定日期过生日的用户一份礼物

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

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

想要知道到底更新了多少个文档,可以运行getLastError命令,键“n”的值就是要找的数字

> db.count.update({x:1},{$inc:{x:1}},false,true)
> db.runCommand({getLastError : 1})
{
     "err" : null,
     "updatedExiting" : true,
     "n" : 5,
     "ok" : true
}

1.3.5 返回已更新的文档

用getLastError仅能获得有限的信息,并不能返回已更新的文档。可以通过findAndModify命令来做到。

findAndModify既有“update”键也有“remove”键。“remove”键表示将匹配到的文档从集合里面删除。如:不要更新状态,直接删掉

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

findAndModify命令中每个键对应的值如下所示。

  • query 

  查询文档,用来检索文档的条件。

  • sort

  排序结果的条件。

  • update

  修改器文档,对找到的文档执行的更新。

  • remove

  布尔类型,表示是否删除文档。

  • new

  布尔类型,表示返回的是更新前的文档还是更新后的文档。默认是更新前的文档。

“update”和“remove”必须有一个,也只能有一个。要是匹配不到文档,这个命令会返回一个错误。这个命令有些限制,一次只能出来一个文档,也不能执行upsert操作,只能更新已有文档。

1.4 瞬间完成

本章的3个操作(插入,删除和更新)都是瞬间完成的,因为他们都不需要等待数据库响应。客户端将文档发送给服务器就立刻干别的了。客户端永远不会收到回应,这个特点的优点是速度快,但服务器崩溃了网线掉了等客户端无法知晓服务器状态下继续发送文档将会造成数据丢失。

1.4.1 安全操作

安全的版本在执行完了操作后立即运行getLastError命令,来检测是否执行成功。驱动程序会等待数据库响应,然后使当地处理错误,一般会抛出一个可被捕获的异常。

具体来说:

  • 如果不考虑安全性的话,就只用离弦之箭的方式;
  • 想要活的稍长一点,就把重要的用户数据(账号,信用卡号,电子邮件)用安全的方式操作,其余的数据就采用离弦之箭的方式;
  • 如果你很谨慎,只用安全操作就好了。

1.4.2 捕获“常规”错误

键重复错误经常发生在试图插入一个其“_id”值已被占用的文档。MongoDB中不允许在一个集合里面有多个“_id”值一样的文档。如果做的是安全插入,发生了键重复错误,安全检查会发现这个服务器错误,并抛出异常。在不安全模式下,数据库没有响应

有时这种插入失败的错误由唯一索引导致,并不一定都由“_id”引发。shell总会检查错误,而驱动程序中检查是可选项。

1.5 请求和连接

数据库会为每一个MongoDB数据库连接创建一个队列,存放这个连接的请求。当客户端发送一个请求,会被放到队列的末尾。只有队列中的请求都执行完毕,后续的请求才会执行。所以从单个连接就可以了解整个数据库,并且它总能读到自己写的东西。

注意,每个连接都有独立的队列,打开两个shell,就有2个数据库连接,一个执行插入,在另一个中查询,不一定能立马查到,但同一个中一定能查到。可能会延迟几秒,使用Ruby,Python和Java驱动程序要特别注意这种行为,因为它们的驱动程序都使用了连接池。为了提高效率,这些驱动程序都和服务器建立了多个连接(一个连接池),并将请求分散到这些连接中去。

posted on 2016-03-10 22:35  gimin  阅读(950)  评论(0)    收藏  举报