如果需要在MongoDB中更新文档,有两种方式,既可以整个替换文档,也可以结合一些更新操作符修改文档中的特定字段。为了替更详细的例子做些铺垫,本章会从一个简单的演示开始,示范这两种做法。随后,我会解释哪种方式更好。
先让我们回忆一下用户示例文档。该文档包含用户的姓名、电子邮件地址和送货地址。毫无疑问,我们时不时会修改一下电子邮件地址,因此就从它开始吧。要完整替换文档,先查询该文档,随后在客户端进行修改,最后用修改后的文档发起更新。以下是对应的Ruby代码:
user_id = BSON::ObjectId(/"4c4b1476238d3b4dd5000001/")doc = @users.find_one({:_id => user_id})doc[/'email/'] = /'[email protected]/'@users.update({:_id => user_id}, doc, :safe => true)
有了用户的_id
,可以先查询文档。接下来在本地进行修改,这里是修改email
属性。随后将修改过的文档传给update
方法。最后一行的意思是“找到users
集合中指定_id
的文档,用我提供的文档替换它”。
以上展示了如何通过替换进行修改,现在让我们看看如何通过操作符进行修改:
@users.update({:_id => user_id}, {/'$set/' => {:email => /'[email protected]/'}}, :safe => true)
本例中使用$set
在一个服务器请求里修改了电子邮件地址,这是多个特殊更新操作符中的一个。这里的更新请求更有针对性:找到指定用户文档,将其email
字段设置为[email protected]
。
其他示例又会如何?这次,我们向用户的地址列表中添加其他送货地址,下面展示如何通过文档替换实现该操作:
doc = @users.find_one({:_id => user_id})new_address = { :name => /"work/", :street => /"17 W. 18th St./", :city => /"New York/", :state => /"NY/", :zip => 10011}doc[/'shipping_addresses/'].append(new_address)@users.update({:_id => user_id}, doc)
更有针对性的做法是这样的:
@users.update({:_id => user_id}, {/'$push/' => {:addresses => {:name => /"work/", :street => /"17 W. 18th St./", :city => /"New York/", :state => /"NY/", :zip => 10011 } } })
替换的方法与之前类似,从服务器获取用户文档,进行修改,随后发回服务器。此处的更新语句和更新电子邮件地址时的一样。相比之下,针对性更新里使用了不同的操作符$push
,将新地址推送到现有的shipping_addresses
数组里。
既然已经看过了几个实际的更新,请思考一下,已经有了一种方法后为什么还要用另一种呢?你觉得哪种方式更直观,哪种方式的性能会更好?
替换更新是种更通用的方式。假设应用程序显示了一个用于更新用户信息的HTML表单,使用替换更新时,从表单提交的数据一经校验就能直接传入MongoDB;无论修改了哪个用户属性,执行更新的代码都是一样的。举例来说,如果你打算构建一个MongoDB对象映射器,需要通用的更新,那么替换更新可能更适合作为默认值。1
1. 大多数MongoDB对象映射器都采用这种策略,原因也很简单。如果用户可以建模任意复杂度的实体,那么发起替换更新比计算特殊更新操作符的理想组合要方便得多。
针对性更新通常性能会更好。首先,不需要在开始时到服务器上获取要修改的文档。其次,指定更新内容的文档一般都很小。如果是通过替换进行更新,文档的平均大小是100 KB,那么每次更新都要向服务器发送100 KB内容!相比之下,上个例子里,无论要修改的文档有多大,每个使用$set
和$push
来指定更新的文档都小于100字节。为此,经常使用针对性更新就意味着节省序列化和传输数据的时间。
此外,针对性操作允许原子性地更新文档。举例来说,如果需要增加计数器值,通过替换进行更新就很不理想;唯一能对它们进行原子性更新的方法就是采用某类乐观锁。在针对性更新中,可以使用$inc
原子性地修改计数器。也就是说,就算有大量的并发更新,每次执行$inc
都是相互隔离的,要么成功,要么失败。2
2. MongoDB文档中使用原子更新(atomic update)这个词来表示我所说的针对性更新(targeted update)。这个新术语意在突出原子这个词。实际上,所有发往核心服务器的更新都是原子性的,以文档为单位进行隔离。说更新操作符是原子性的是因为它们能在不用先查询的情况下更新文档。
乐观锁
乐观锁即乐观并发控制,这项技术保证在无需锁定记录的情况下对其进行彻底更新。要理解它,最简单的方法是想象一个wiki,有多个用户可以同时编辑一个wiki页面,但你肯定不希望用户编辑并更新一个过期的页面,这时可以使用乐观锁协议。当用户试图保存他们的变更时,会在更新操作中包含一个时间戳,如果该值比这个页面最近保存的版本旧,那么不能让用户进行更新;但如果没人修改过这个页面,则允许更新。该策略允许多个用户同时编辑一个页面,比另一种要求每个用户在编辑任意页面时获得一个锁的并发策略要好很多。
知道了可用的更新种类之后,你就能理解下一节里我将介绍的策略了。下一节中,我们会回到电子商务数据模型,回答一些与在生产环境中操作数据相关的、更困难的问题。