章节:第三章 创建、更新及删除文档

内容:

  1. 插入并保存文档
  1. 使用insert方法:如
db.foo.insert({"bar" : "baz"})
  1. 批量插入
  1. 批量插入能传递一个由文档构成的数组给数据库,且一次批量插入只是单个的TCP请求,无需处理大量的消息头,避免了许多零碎的请求所带来的开销
  2. 只有插入多个文档到一个集合时,才能提高效率,而不能用批量插入一次对多个集合执行操作
  3. 要是只导入原始数据,可以使用命令行工具,如mongoimport,而不是批量插入
  1. 插入:原理和作用
  1. 当执行插入时,使用的驱动程序会将数据转换成BSON形式,然后将其送入数据库
  2. 数据库解析BSON,检验是否包含“_id”键并且文档不超过4MB
  3. MongoDB在插入时不做别的数据校验,也不执行代码,所以也就没有注入式***的可能,但副作用是允许插入无效的数据
  1. 删除文档
  1. 删除特定文档,如
db.mailing.list.remove({"opt-out" : true})
  1. 删除集合中的所有文档,如db.users.remove(),但不会删除集合本身,原有的索引也会保留
  2. 删除速度:通常删除文档会很快,但若要清楚整个集合的文档,则直接删除集合(然后重建索引)会更快,如db.drop_collection("bar")即是删除集合bar
  1. 更新文档
  1. 更新文档的方法update有2个参数,一个是查询文档,用来找出要更新的文档,另一个是修改器文档,描述对找到的文档做哪些更改
  2. 更新操作是原子的,若是两个更新同时发生,先到达服务器的先执行,接着执行另外一个
  3. 文档替换:
  1. 更新最简单地情形就是完全用一个新文档替代匹配的文档,这适用于模式结构发生了较大变化时,如
db.users.update({"name" : "joe"} , joe)
  1. 常见错误就是查询条件匹配了多个文档,然后更新的时候由于第二次参数的存在就产生重复的“_id”值,数据库会报错,不做任何修改
  1. 使用修改器
  1. 通常文档只会有一部分要更新,利用原子的更新修改器,可以使得这种部分更新极为高效
  2. 更新修改器是种特殊的键,用来指定复杂的更新操作,比如调整、增加或者删除键,还可以操作数组或者内嵌文档
  3. 使用修改器时,"_id"的值不能改变(而整个文档替换时是可以改变的);其他键值,包括其他唯一索引的键,都是可以更改的
  4. "$set"修改器:“$set”用来指定一个键的值,若键不存在,则创建它,如
db.users.update({"name":"joe"},...{"$set":{"favorite book":"green eggs and ham"}})

也可以用“$unset”将键完全删除,如

db.users.update({"name":"joe"},{"$unset":{"favorite book":1}})
  1. "$inc"修改器:“$inc”修改器用来增加已有键的值,或者在键不存在时创建一个键,如
db.games.update({"game":"pinball","user":"joe"},...{"$inc":{"score":50}})

“$inc”只能用于整数、长整数或双精度浮点数,用在其它类型的数据上会导致操作失败;另外“$inc”键的值必须为数字,不能使用字符串、数组或其他非数字的值

  1. 数组修改器:用在值为数组的键上,如果指定的键已存在,"$push"会向已有的数组末尾加入一个元素,要是没有就会创建一个新的数组,如
db.blog.posts.update({"title":"A blog post"},{$push:{"comments":...{"name":"joe","email":"joe@example.com","content":"nice post."}}})

如果一个值不在数组中再将其加进去,可以用“$ne”或“$addToSet”实现,如

db.papers.update({"authors cited":{"$ne":"Richie"}},...{"$push":{"authors cited":"Richie"}})
或
db.users.update({"_id":ObjectId("4b2d75476cc6113d5ee930164")},...{"$addToSet":{"emails":"joe@gmail.com"}})

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

db.users.update({"_id":ObjectId("4b2d75476cc6113d5ee930164"},{"$addToSet":...{"emails":{"$each":["joe@php.net","joe@example.com","joe@python.org"]}}})

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

有时需要基于特定条件来删除元素,而不仅仅是依据位置,“$pull”可以做到,“$pull”会将所有匹配到的部分删除,如

db.lists.update({},{"$pull":{"todo":"laundry"}})
  1. 数组的定位修改器:若是数组有多个值,而我们只想对其中的一部分进行操作,有2种方法可以实现:通过位置或者定位操作符(“$”),如
#下标为0表示数组的第一个元素
db.blog.update({"post":post_id},...{"$inc":{"comments.0.votes":1}})
#在大多数情况下,不预先查询文档就不能知道要修改数组的下标,故MongoDB提供了定位操作符“$”,用来定位查询文档已经匹配的元素,并进行更新
db.blog.update({"comments.author":"John"},...{"$set":{"comments.$.author":"Jim"}})
#定位符只更新第一个匹配到的元素
  1. 修改器速度:$inc能就地修改,且不需要改变文档的大小,故运行非常快,而数据修改器可能更改了文档的大小,就会慢一些;MongoDB预留了些补白给文档,来适应大小变化,但若超出了原来的空间,则会分配新的空间,就会减慢速度,同时随着数组变长,MongoDB需要更长的时间来遍历整个数组,对每个数组的修改也会慢下来。
  1. upsert:是一种特殊的更新,若是没有文档符合更新条件,就会以这个条件和更新文档为基础创建一个新的文档,若匹配到了文档,则正常更新;update操作的第3个参数表示是否开启upsert,如
db.analytics.update({"url":"/blog"},{"$inc":{"count":3}},true)

save Shell帮助程序:save是一个shell函数,可以在文档不存在时插入,存在时更新,只有一个参数:文档;若这个文档含有“_id”键,save会调用upsert,否则调用插入,如

var x = db.foo.findOne()
x.num = 42
db.foo.save(x)
  1. 更新多个文档:默认情况下,更新只能对符合匹配条件的第一个文档执行操作,若使所有匹配到的文档都得到更新,可以设置update的第4个参数为true,如
db.users.update({birthday:"10/13/1978"},...{$set:{gift:"Happy Birthday!"}},false,true)

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

db.count.update({x:1},{$inc:{x:1}},false,true)
db.runCommand({getLastError:1})
  1. 返回已更新的文档:用getLastError仅能获得有限的信息,并不能返回已更新的文档,可通过findAndModify命令实现,其调用方式和普通的更新略有不同,有点慢,因为它要等待数据库的响应,如
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命令中每个键的对应值:
findAndModify:字符串,集合名
query:查询文档,用来检索文档的条件
sort:排序结果的条件
update:修改器文档,对所找到的文档执行的更新
remove:布尔类型,表示是否删除文档
new:布尔类型,表示返回的是更新前的文档还是更新后的文档,默认是更新前的文档
#findAndModify命令的限制:
#"update"和"remove"必须有一个,也只能有一个;
#若匹配不到文档,则返回错误
#一次只能处理一个文档,也不能执行upsert操作,只能更新已有文档


瞬间完成:本章讨论的3个操作(插入、删除、更新)都是瞬间完成的,因为它们都不需要等待数据库响应,只会受客户端发送的速度和网络速度的制约;这对于某些应用(如日志记录,分析数据等)是可以接受的,但对于某些应用(如付费系统)就不适用了

  1. 安全操作
  1. 若要完成电子商务系统,则需要一个“安全”版本,保证执行时检查到了错误还可以重来;
  2. 安全的版本在执行完了操作后立即运行getLastError命令,来检查是否执行成功,驱动程序会等待数据库响应,然后适当地处理错误,一般会抛出一个可被捕获的异常
  3. “安全”的代价就是性能,即便忽略客户端处理异常的开销,等待数据库响应本身的时间比只发送消息的时间多一个数量级,所以,应用程序需要权衡数据的重要性(以及丢失后的后果)及速度需求
  1. 捕获“常规”错误
  1. 安全操作也是一种调试数据库“奇怪”行为的好方法,这样可以避免很多常见的数据库使用错误
  2. 最常见的就是键重复的错误,即插入一个“_id”值已被占用的文档

请求和连接

  1. 数据库会为每一个MongoDB数据库连接创建一个队列,存放这个连接的请求
  2. 当客户端发送一个请求,会被放到队列的末尾,只有队列中的请求都执行完毕,后续的请求才会执行
  3. 所以从单个连接就可以了解整个数据库,并且它总是能读到自己写的东西
  4. 每个连接都有独立的队列,要是打开2个shell,就有2个数据库连接,在一个shell中执行插入,之后在另一个shell中执行查询不一定能得到插入的文档
  5. 使用Ruby、Python和Java驱动程序时要特别注意这种行为,因为这几种语言的驱动程序都使用了连接池