MonggoDB In Action-更新、原子操作与删除(Part3)

来源:互联网 发布:什么是数据api接口 编辑:程序博客网 时间:2024/06/05 15:34
db.products.update({slug:'shovel'},{$push:{'tags','tools'}})

具体细节:MongoDB的更新与删除
     要真正理解掌握MongoDB中的更新,需要彻底理解MongDB的文档模型和查询语言。
4.1 更新类型与选项

    MongoDB支持针对性更新与替换更新。前者用一个或多个更新操作符来定义,后者使用一个文档来替换匹配更新查询选择器的文档。针对性更新中,更新操作符时前缀,而查询操作符通常是中缀。

   请注意,如果更新文档含糊不清,更新将会失败。此处,我们将更新操作符$addToSet和替换风格的{name:"Pitchfork"}结合在一起:

db.product.update({},{name:"Picthfork",$addToSet:{tags:'cheap'}})
   如果目的是为了更新文档名字,必须使用$set操作符

db.products.update({},{'$set':{name:"Pitchfork"},$addToSet:{tags:'cheap'}})
4.1.1 多文档更新

       默认情况下,更新操作只会更新查询选择器匹配到的第一个文档。要更新匹配到的所有文档,需要明确指出多文档更新(mutidocument update)。在shell中,要实现这一点,可以将update方法的第四个参数设置为true。下面展示如何为产品集合里的所有文档添加cheap标签

db.products.update({},{'$addToSet':{tags:'cheap'}},false,true)
使用Ruby驱动时,可以更清楚的表示多文档更新

products.update({},{'$addToSet'=>{'tags'=>'cheap'}},:muti=>true)
4.1.2 upsert

   某项内容不存在时进行插入,存在时进行更新,这是很常见的需求。可以使用MongoDB的upsert轻松实现这一模式。如果查询选择器匹配到文档,进行普通的更新操作。如果没有匹配到文档,则插入一个新文档。新文档的属性合并自查询选择器与针对性更新的文档。

以下是在shell中使用upsert的简单示例

db.products.update({slug:'hammer'},{$addToSet:{tags:'cheap'}},true)
在Ruby中等效的upsert操作

products.update({'slug'=>'hammer'},{'$addToSet'=>{'tags'=>'cheap'}},:upsert=>true)
你应该猜到了,upsert一次只能插入一个或更新一个文档。在需要原子性地更新文档,以及无法确定文档是否存在时,upsert能发挥巨大的作用。

4.2 更新操作符

MongoDB支持很多更新操作符,此处我们会为每个更新操作符提供一个简单的示例

4.2.1 标准更新操作符

  第一组是常用的操作符,几乎能用于任意数据类型

(1) $inc

可以使用$inc操作符递增或递减数值

db.products.update({slug:'shovel'},{$inc:{review_count:1}})db.users.update({username:'moe'},{$inc:{password_retires:-1}})
也可以使用它来加减任意数字
db.readings.update({_id:324},{$inc;{temp:2.7435}})
$inc既方便又高效,因为它很少会改变文档的大小,$inc通常原地作用在数据的磁盘位置上,所以只会影响到指定的数据树。正如添加产品到购物车的示例中演示的那样,$inc能用于upsert中。例如可以将之前的更新更新为upsert

db.readings.update({_id:324},{$inc;{temp:2.7435}},true)
(2) $set和$unset

如果需要为文档中的特定键赋值,可以使用$set。为键赋值时,可以使用任意合法的BSON类型。也就是说以下更新都是正确的:

db.reading.update({_id:324},{$set:{temp:97.6}})db.reading.update({_id:325},{$set:{temp:{f:212,c:100}}})db.reading.update({_id:326},{$set:{temps:[97.6,98.4,99.1]}})
如果键已存在,其值会被覆盖;否则会创建一个新的键
$unset能删除文档中特定的键。下面展示例如如何删除文档中的temp键:

db.reading.update({_id:324},{$unset:{temp:1}})
还可以在内嵌文档和数组之上使用$unset,这两种情况都要用点符号指定内部对象。如果集合中有两个文档:

{_id:325,'temp':{f:212,c:100}}{_id:326,temps:[97.6,98.4,99.1]}
我们可以使用下面的语句删除第一个文档里的华氏温标读数,以及第二个文档中的第0个元素:

db.reading.update({_id:325},{$unset:{'temp.f':1}})db.reading.update({_id:326},{$pop:{temps:1}})
执行前后的查询结果如下图,测试过程中使用的是temp集合


$set也能使用访问子文档和数组元素的点符号。

(3) $rename

如果要更改键名,请使用$rename

db.readings.update({_id:324},{$rename:{'temp':'temperature'}})
还可以重命名子文档

db.readings.update({_id:324},{$rename:{'temp.f':'temp.farenheit'}})
请注意,在单个数组元素上使用$unset的结果可能与你设想的不一样。其结果知识将元素的值设置为null,而非删除整个元素。要彻底删除某个数组元素,可以使用$pop和$pull操作符

db.reading.update({_id:325},{$unset:{'temp.f':1}})db.reading.update({_id:325},{$unset:{'temp.0:1}})

4.2.2 数据更新操作符

    数组在MongoDB文档模型中的重要性是显而易见的,因此MongoDB理所当然地提供了很多专门用于数组的更新操作符:

(1) $push和$pushAll

    如果需要为数组追加一些值,可以考虑$push和$pushAll,前者能向数组中添加一个值,而后者则支持添加一个值列表。例如,可以很方便地为铲子添加新标签:

db.products.update({slug:'shovel'},{$spush:{'tags':'tools'}})
    如果需要在一个更新中添加多个标签,同样不成问题:

db.products.update({slug:'shovel'},{$pushAll:{'tags':['tools','dirt','garden']}})
注意,可以往数组里添加各种类型的值,不局限于标量(scalar)。上一节里,向购物车的明细条目数组中添加产品代码就是一个很好的例子。

(2) $addToSet与$each

     $addToSet也能为数组追加值,不过它的做法更细致:要添加的值如果不存在才执行添加操作。因此,如果铲子已经有了tools标签,那么以下更新就不会修改文档

db.products.update({slug:'shovel'},{$addToSet:{'tags':'tools'}})
如果想要在一个操作中向数组添加多个唯一的值,必须结合$each操作符来使用$addToSet.下面是一个示例

db.products.update({slug:'shovel'},{$addToSet:{'tags':{$each:['tools','dirt','steel']}}})
仅当$each的值不在tags里时,才会进行添加。

(3) $pop

   要从数组中删除元素,最简单的方法就是使用$pop操作符。如果用$push向数组中追加了一个元素,那么随后的$pop会删除最后添加的内容。虽然$pop和$push一起出现,但也可以单独使用。如果tags数组里包含['tools','dirt','garden','steel']这四个值,那么下面的$pop会删除steel标签:

db.products.update({slug:'shovel'},{$pop:{'tags':1}})
$pop的语法和$unset类似,即{$pop:{'elementToRemove':1}},不同的是$pop还能接受-1来删除数组的第一个元素。下面展示如何从数组中删除tools标签:

db.products.update({slug:'shovel'},{$pop:{'tags':-1}})
可能有一点让你不太满意,即无法返回$pop从数组中删除的值。尽管它的名字叫$pop,但其结果和你所熟知的栈式操作不太一样,请注意这一点
(4) $pull和$pullAll

    $pull的作用与$pop类似,但更高级一点。使用$pull时可以明确用值来指定要删除哪个数组元素,而不是位置。再来看看标签示例,如果要删除dirt标签,无需知道它在数组中的位置,只需告诉$pull操作符删除它就可以了。

db.products.update({slug:'shovel'},{$pull:{'tags':'dirt'}}})
$pullAll类似于$pushAll,允许提供一个要删除值的列表。如果要删除dirt和garden标签,可以这样使用$pushAll:

db.products.update({slug:'shovel'},{$pullAll:{'tags':['dirt','garden']}}})
4.2.3 位置更新
   在MonggoDB中建模数据时会使用子文档数组,但在位置操作符出现之前,要操作哪些子文档并非易事。位置操作符允许更新数组里的子文档,我们可以在查询选择器中用点符号指明要修改的子文档。假设有如下文档:

{_id: new ObjectId("6a5b1476238d3b4dd5000048"),line_item:[{_id:ObjectId("4c4b1476238d3b4dd5003981"),sku:"9092",name:"Extra Large Wheel Barrow",quantity:1,pricing:{retail:5897,sale:4897}},{_id:ObjectId("4c4b1476238d3b4dd5003981"),sku:"10027",name:"Rubberized Work Glove,Black",quantity:2,pricing:{retail:1499,sale:1299}}]}
假设想设置第二个明细条目的数量,把SKU为10027那条的数量设置为5.问题是你不清楚这个特定的子文档在line_items数组里的位置,甚至不知道它是否存在。只需一个简单的查询选择器,以及一个简单的位置操作符的更新文档,这些问题就迎刃而解了:

query={_id:ObjectId("4c4b1476238d3b4dd5003981"),'line_items.sku':"10027"}update={$set:{'line_items.$.quantity':5}}db.orders.update(query,update)
在‘line_items.$.quantity’字符串里看到的$就是位置操作符,如果查询选择器匹配到了文档,那么有10027这个SKU的文档的下表就会替换位置操作符,从而更新正确的文档。如果数据模型中包含了子文档,那么你会发现在执行精细的文档更新操作时,位置操作符是在太有用了。

4.2.4 findAndModify命令

    下面的是findAndModify的参数:

  query: 文档查询选择器,默认为{}

  update:描述更新的文档,默认为{}

  remove:布尔值,为true时删除对象并返回,默认为false

  new:布尔值,为true时返回修改后的文档,默认为false

  sort:指定排序方向的文档,因为findAndModify一次只修改一个文档,sort选项能用来控制处理哪个文档。例如,可以按照{create_at:-1}来排序,处理最近创建的匹配文档。

  fields;如果只需要返回字段的子集,可以通过该选项指定。当文档很大时,这个选项很有用。能像在其他查询里一样指定字段。

  upsert:布尔值,为true时将findAndModify当做upsert对待。如果文档不存在,则创建之。请注意如果你希望返回新创建的文档,还需指定new:true

4.2.5 删除

     我们可以删除整个集合,也可以向remove方法传递一个查询选择器,删除集合的子集。删除全部评论时很容易的:

db.reviews.remove()
但更常见的做法是删除特定用户的评论

db.review.remove({user_id:ObjectId("4c4b1476238d3b4dd5000001")})
所有对remove的调用都接受一个可选的查询选择器,用于指定要删除的文档。
4.2.6 并发性、原子性与隔离性

   理解MongoDB中如何保证并发性是很重要的。锁策略非常粗放,靠一个全局读写锁来控制整个mongod实例。这样意味着,任何时刻,数据库只允许存在一个写线程或多个读线程(且两者不能并存)。锁策略还有一些并发优化措施。其中之一是,数据库持有一个内部映射,知道哪些文档在内存里。对于哪些不在内存里的文档的读写,数据库会让步于其他操作,知道文档被载入内存。

  第二个优化就是写锁让步。任何写操作都可能耗时很久,所有其他的读写操作在此期间都会被阻塞。所有的插入、更新和删除都要持有写锁。插入的耗时一般不长,但更新就不一样的了,比方说更新整个集合需要很久,涉及很多文档的删除操作也是如此。当前的解决方法时允许这些耗时很久的操作周期性地暂停,以便执行其他的读和写。在操作暂停时,它会自己停下来,释放锁,稍后再恢复。

    但在更新和删除文档时,这种暂停行为可能好坏参半。很容易考虑这种情况:希望在其他操作发生前更新或删除所有文档。在这些情况下,可以使用名为$atomic的特殊操作选项来避免暂停。简单地在查询选择器中添加$atomic操作符即可:

db.reviews.remove({user_id:ObjectId("4c4b1476238d3b4dd500001")},{$atomic:true})
对于多文档更新,也可以做同样的处理,这迫使整个多文档更新在隔离情况下执行完毕:

db.reviews.remove({$atomic:true},{$set:{rating:0},false,true})
这个更新操作将所有的评论的评分设为0,因为操作时隔离执行的,所以不会暂停,保证系统始终是一致的。

4.2.7 更新性能说明

    经验表明,对更新是如何作用于磁盘上的文档能有一个基本认识,有助于设计出性能更好的系统。你应该理解的第一件事实何种程度的更新能被成为“原地”更新。理想情况下,在磁盘上,更新对一个BSON文档的影响只是极小一部分,这样的性能是最好的,但事实并非总是如此。

    磁盘上的文档更新本质上分为三种:第一种,也是最高效的,只发生在单值修改并且整个BSON文档的大小不改变的情况下。这通常会发生在$inc操作符上,因为$inc只会增加一个整数,该值在磁盘上的大小并不改变。如果这个整数是由int表示的,那么他会占用四个字节;长整数和双精度浮点数会占用八个字节。更改这些数字的值并不需要更多空间,因此磁盘上就只需要重写该文档的一个值。

   第二种更新会改变文档的大小和结构。BSON文档会表示为字节数组,文档的头四个字节总是存储文档的大小。因此,当在文档上使用$push操作符时,不仅增加了整个文档的大小,还改变了它的结构。这要求在磁盘上重写整个文档,这么做的效率还不算太差,但还是应该注意一下。如果在一个更新中使用了多个更新操作符,那么每个操作符都会重写一次文档。这也通常不算什么大问题,尤其是写操作发生在内存里时。但如果文档特别大,比如有4MB左右,而你又在使用$push向那些文档里添加值,那么服务器端就可能要做很多事情了。

最后一种更新是重写文档的结果。如果文档扩大了,无法放入之前分配的磁盘空间里,那么该文档不仅要重写,而且还必须移到新的空间里。这种移动操作如果经常发生,代价还是很大的。为了降低此类开销,MongoDB会根据每个集合的情况动态调整填充因子(padding factor)。也就是说,如果有一个集合会发生很多重新分配空间的更新,则会增加其内部填充因子。填充因子乘上插入文档的大小后就能得到额外创建的空间。这能减少未来重新分配文档的数量。

要想查看指定集合的填充因子,可以使用stats命令:

db.tweets.stats(){"ns":"twitter.tweets","count":53641,"size":85794884,"avgObjSize":1599.4273783113663,"storageSize":100375552,"numExtents":12,"nindexes":3,"lastExtentSize":21368832,"paddingFactor":1.2,"flags":0,"totalIndexSize":7946240,"indexSizes":{"_id_":2236416,"user.friends_count_1":1564672,"user.screen_name_1_user.created_at_-1":4145152}}
从结果显示可以看出,这一集合的填充因子是1.2,即插入100B的文档时,MongoDB会在磁盘上分配120B。默认的填充因子是1,即不会分配额外空间。








 

















原创粉丝点击