第二章 - 更新

在第一章中我们介绍了CRUD(Create、Read、Update、Delete)中的三个操作。本章专门用来介绍前面跳过的第四个操作:updateupdate有一些出人意料的行为,这就是为什么我们专门在这章当中讨论它。

update: replace 与 $set

update最简单的执行方式有两个参数:一个是选择器(选择更新的范围),一个是需要更新的域。如果Roooooodles长胖了,我们就需要:

  1. db.unicorns.update({name: 'Roooooodles'}, {weight: 590})

(如果您用的是自己创建的unicorns集合,原来的数据都丢失了,那么就用remove删除掉所有文档,重新插入第一章中的数据)

在实际的代码中,您也许会基于_id来更新记录,不过既然我不知道MongoDB给您分配的_id是什么,我们就用name好了。如果我们看看更新过的记录:

  1. db.unicorns.find({name: 'Roooooodles'})

您就会发现update第一个出人意料的地方:上面的命令找不到任何文档。这是因为命令中输入的第二个参数是用来替换(replace)原来的文档的。换句话说,update先是根据name找到一个文档,然后用新的文档(也就是第二个参数)去覆盖找到的整个文档。这和SQL中的update的行为是不一样的。在某些情况下,这一行为非常理想,可以用于实现完全动态的更新。然而当您需要的仅是改变某个文档的某个值或者几个域,最好还是用MongoDB的$set修改符(modifier):

  1. db.unicorns.update({weight: 590}, {$set: {name: 'Roooooodles', dob: new Date(1979, 7, 18, 18, 44), loves: ['apple'], gender: 'm', vampires: 99}})

这样做就会重设那些丢失的域。新的weight值不会被覆盖,因为我们没有在命令中指定它。如果现在执行:

  1. db.unicorns.find({name: 'Roooooodles'})

得到的就是预想的结果。所以在一开始时正确的更新体重的方法应该是:

  1. db.unicorns.update({name: 'Roooooodles'}, {$set: {weight: 590}})

更新修改符

除了$set,还有其他的修改符可以用来非常漂亮地完成一些任务。所有的更新修改符都作用于域上——这样您的文档就不会被整个改写。比如说,$inc可以用来将一个域的值增加一个正的或负的数值。举个例子,如果由于失误,Pilot多获得了一些吸血的技能(vampire skill。译者:这里的独角兽可以理解为游戏中的某个角色,而这个角色也许可以通过升级打怪的方式提升某些技能,比如说吸血技能。),我们可以用下面的命令来纠正这个错误:

  1. db.unicorns.update({name: 'Pilot'}, {$inc: {vampires: -2}})

如果Aurora忽然长出了一颗可爱的牙(译者:可以吃糖了),可以用$push修改符为她的loves域添加一个新的值:

  1. db.unicorns.update({name: 'Aurora'}, {$push: {loves: 'sugar'}})

MongoDB网站上的Updating部分可以找到其他更新修改符的更多信息。

插新(Upsert)

译者:Upsert的意思是update if present; insert if not。是update和insert合体的产物。没有找到一个合适的词作为翻译,于是我斗胆发明了“插新”这个词,取或插入或更新之意。如有更好的办法,还请指点。

update的一个比较讨喜的出人意料之处就是它完全支持插新(upsert)。当目标文档存在的时候,插新操作会更新该文档,否则就插入该新文档。插新在某些情况下是很方便的,当您碰到这种情况的时候就会知道了。为了打开插新的功能,我们在使用update时把第三个参数设为true

一个很常见的例子就是网站的点击计数器。如果需要得到实时的点击累计数值,我们需要知道这个页面的点击记录是否存在,然后决定是要更新点击数还是插入。如果忽略第三个参数(或者是设置为false),下面的命令什么也不做:

  1. db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}});
  2. db.hits.find();

如果打开了插新,结果就不一样了:

  1. db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, true);
  2. db.hits.find();

这一次,因为没有文档有域page的值为unicorns,就插入一个新的文档。再执行一次上面的命令,创建好的文档就会被更新,而hits的值就会增加为2。

  1. db.hits.update({page: 'unicorns'}, {$inc: {hits: 1}}, true);
  2. db.hits.find();

多重更新

update最后的一个惊喜是,它会默认地只更新一个文档。到目前为止就我们所见到的例子来看,这样做是合理的。不过如果执行下面的命令:

  1. db.unicorns.update({}, {$set: {vaccinated: true }});
  2. db.unicorns.find({vaccinated: true});

您想要做的应该是找出所有已经注射过疫苗(vaccinated)的独角兽,但为了达到这样的目的,需要把第四个参数设为true:

  1. db.unicorns.update({}, {$set: {vaccinated: true }}, false, true);
  2. db.unicorns.find({vaccinated: true});

本章小结

本章完成了对基础的集合操作,CRUD的介绍。我们细致的了解了update以及它的三个有意思的行为:第一,和SQL的update不同,MongoDB的update会替换实际的文档。因此$set修改符就显得很有用了。第二,update支持直观的插新,这在和$inc修改符结合起来的时候特别有用。最后,update的默认行为是只更新第一个找到的文档。

一定要记住的是,我们是在MongoDB的shell中介绍它的。实际应用时您所采用的驱动或是库有可能会修改这些默认的行为,或是提供一个不同的编程接口(API)。例如:Ruby的驱动把最后的两个参数合并为一个哈希表:{:upsert => false, :multi => false}。类似地,PHP的驱动把最后的两个参数合并到了一个数组中:array('upsert' => false, 'multiple' => false)