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

       上一篇In Action章节介绍了文档的查询操作,介绍了查询的相关的操作符,聚合操作等知识点,本章我们接着上面的章节分享MongoDB的更新、原子操作与删除等知识点。

1 文档更新入门

      如果在MongoDB中更新文档,有两种方式,既可以是整个替换文档,也可以结合一些更新操作符修改文档中的特定字段。让我们从例子中来分析哪种方式更好。前面章节的用户文档中,包含了用户的姓名、电子邮件和送货地址。毫无疑问,我们会经常替换电子邮件。要替换完整文档,先查询该文档,随后在客户端进行修改,最后用修改后的文档发起更新,对应的Ruby代码如下:

user_id=BSON::ObjectId("4c4b1476238d3b4dd5000001")
doc=users.find_one(:id=>user_id)
doc['email']='mongodb-user@10gen.com'
users.update({:_id=>user_id},doc,:safe=>true)
有了用户的_id,可以先查询文档。接下来进行本地修改email属性,随后将修改的文档传递给update方法。最后一行的意思是"找到users集合中指定_id的文档,用新提供的文档替换它“。上面展示了如何通过替换修改,下面的实例是通过操作符进行修改:
users.update({:_id=>user_id},{'$set'=>{:email=>'mongodb-user@10gen.com'}},:safe=>true)
      本例中使用的$set在一个服务器请求里修改了电子邮件地址,这是多个特殊更新操作符中的一个。这里的更新请求更有针对性:找到指定用户文档,将其email字段设置为mongodb-user@10gen.com
     若应用场景变为要向用户地址列表中添加其送货地址,文档替换的操作方式:

doc=users.find_one({:id=>user_id})
new_address={
             :name=>"work",
             :street=>"17 W18 th 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'=>{
:shipping_addresses=>
{
:name=>"work",
:street=>"17 W18 th st",
:city=>"New York",
:state=>"NY",
:zip=>10011
}
}
})

      文档替换方式与之前的操作方式类似,从服务器获取用户文档,进行修改,随后发回服务器。此处的更新语句和更新电子邮件时的一致。相比之下,针对性更新使用了不同的操作符$push,将新地址推送到现有的shipping_address数组里。

     替换更新是种更通用的方式。假设应用程序显示了一个用于更新用户信息的HTML表单,使用替换更新时,从表单提交的数据就能直接传入MonggoDB;无论修改了哪个用户属性,执行更新的代码都是一样的。举例来说,如果你打算构建一个MongoDB对象映射器,需要通用的更新,那么替换更新可能更适合作为默认值。

    针对性更新通常性能更好。首先,不需要开始到服务器上获取要修改的文档;其次,指定更新内容的文档一般都很小。如果是通过替换进行更新,文档的平均大小是100KB,那么每次更新都要发送100KB内容。相比之下,使用$set或者$push来指定更新的文档都小于100KB。为此,经常使用针对性更新就意味着节省序列化和数据传输的时间。

    此外,针对性操作允许原子性地更新文档。举例来说,如果需要增加计数器值,通过替换进行更新就不是很理想;唯一能对它们进行原子性更新的方法就是采用某类乐观锁。在针对性更新中,可以使用$inc原子性地修改计数器。也就是说,就算有大量的并发更新,每次执行$inc都是相互隔离的,要么成功,要么失败。说针对性操作具有原子性,是因为更新操作符能在不用查询的情况下更新文档。

   乐观锁即乐观并发控制,这项技术保证在无需锁定记录的情况下对其进行彻底更新。要理解乐观锁,最简单的方法时想象一个wiki,有多个用户可以同时编辑一个wiki界面,但你肯定不希望用户编辑并更新一个过期的页面,这是可以使用乐观锁协议。当用户试图保存他们的更新是,会在更新操作中包含一个时间戳,如果改值比这个页面最近保存的版本旧,那么就不能让用户进行更新;但如果没人修改过这个页面,则允许更新。该策略允许多个用户同时编辑一个页面,比另一种要求每个用户在编辑任意页面时获得一个锁的并发策略要好很多。

 2 电子商务数据模型中的更新

       在股票的例子中更新MongoDB文档中的这个或那个属性是很容易的。但生产环境中的数据模型和真实的应用程序中,会出现不少困难,对指定属性的更新可能不再是一种简单的一行语句。

2.1 产品与分类

     了解如何计算产品评分,随后是更复杂的维护分类层级的任务

2.1.1 产品平均评分

      产品可以运用很多更新策略。假设管理员有一个界面可用于编辑产品信息,最简单的更新涉及获取当前产品文档,将其与用户编辑的文档进行合并,执行一次文档替换。有时候,可能只需更新几个值,显然这是针对性更新是更好的选择。计算产品平均评分就是这种情况。因为用户会基于平均评分对产品列表排序,可以将该评分保存在产品文档中,在添加和删除评论时进行更新

average=0.0
count=0
total=0
cursor=reviews.find({:product_id=>product_id},:fileds=>["rating"])
while cursor.has_next?&&review=cusor.next()
    total+=review['rating']
    count+=1
end
average=total/count
products.update({:id=>BSON::ObjectId("4c4b1476238d3b4dd5003981")},{'$set'=>{:total_review=>count,:avergae_review=>average}})
    这段代码聚合并处理了每条产品评论中的rating字段,然后计算了平均值。实际上,我们迭代了每个评分,借此计算产品的总评分,这节省了一次额外的count函数调用。有了评论的总条数和平均评分后,在代码中使用$set执行一次针对性更新。
2.1.2 分类层级

     在很多数据库中都没有简单的方法来表示分类层级,虽然文档结构对此有所帮助,但MongoDB里的情况也差不多。文档可以针对读取进行优化,因为每个分类都能包含其祖先的列表。唯一麻烦的要求是始终保持最新的祖先列表。

     首先需要一个通用的方法更新任意给定分类的祖先列表,下面是一个可行方案:

def generate_ancestors(_id,parent_id)
   ancestor_list=[]
   while parent =catagories.find_one({:id=>parent_id}) do
       ancestor_list.unshift(parent)
       parent_id=parent['parent_id']
    end
    categories.update({:_id=>_id}=>{"$set"=>{:ancestors=>ancestor_list}})
end

    该方法回溯了分类层级,连续查询每个节点的parent_id属性,知道根节点(parent_id是nil的节点)为止,总之,它构建了一个有序的祖先列表,保存在ancestor_list数组里。最后,使用$set更新操作符更新分类的ancestors属性。

  既然有了基本的构建模块,那就让我们来看看插入新分类的过程,假设一个简单的分类层级如下图所示


假设想在Home分类下添加一个名为Gardening的新分类,插入新文档后运行方法来生成它的祖先列表

category=
{
:parent_id=>parent_id,
:slug=>"gardening",
:name=>"Gardening",
:description=>"All gardening implements,tools,seeds,and soil"
}
gardernt_id=categories.insert(category)
generate_ancestors(gardening_id,parent_id)
下图显示的是更新后的树


这太简单了,如果想要把Outdoors分类放在Gardening下面,这是就需要修改分类的祖先列表,因为要修改很多分类的祖先列表。可以从把Outdoors的parent_id修改为Gardening的_id开始做起,这还不是很困难。

categories.update({:_id=>outdoors_id},{'$set'=>{:parent_id=>gardening_id}})
因为移动了Outdoors分类,所以其所有后代的祖先列表都无效了。可以查询所有祖先列表里有Outdoors的分类,随后重新生成它们的祖先列表。MongoDB可深入数组进行查询,因而能轻而易举地完成这项工作。

categories.find({'ancestors_id'=>outdorrs_id}).each do |category|
  generate_ancestors('_id',outdoors_id)
end
下图展示的是变更后的类的排列方式:


  如果要想修改分类名称又会怎么样?如果将Outdoors分类的名称修改为The Great Outdoors,那么还必须修改其祖先列表中出现Outdoors的分类。这是你会想“看到没?这种情况下去正规化就麻烦了”。但了解到不用重新计算祖先列表就能执行这个更新后,你应该会感觉好很多,方法如下:

doc=categories.find_one({:_id=>outdoors_id})
doc['name']="The Great Outdoors"
categories.update({:_id=>outdoors_id},doc)
categories.update({'ancestors.id'=>outdoors_id},{'$set'=>{'ancestors.$'=>doc}},:muti=>true)
我们先取得了Outdoors文档,在本地修改它的名字,随后使用替换更新,最后再用修改后的Outdoors文档来替换多个祖先列表中的旧文档。通过位置操作符和多项更新实现了这个操作。多项更新很容易理解;回忆一下, 如果希望修改能作用于所有选择器匹配到的文档,需要指定:muti=>true。此处,我们想要更新所有祖先列表中有Outdoors的分类。位置操作符更巧妙一些, 假设我们无法获知Outdoors分类会出现在给定分类祖先列表中什么地方,此时就需要更新操作符针对指定文档动态定位Outdoors分类在数组中的位置。位置操作符即ancestors.$中的$,代替查询选择器匹配到的数组下标,这才使这个更新操作成为可能。

       因为需要更新数组中的单独的子文档,总会用到位置操作符。总的来说,在要处理子文档数组时,这些更新分类层级的技术都能适用

2.2 评论

     评论并不是完全“平等的”,这就是应用程序允许用户对评论进行投票的原因。投票很简单,他们指出了哪些评论是有用的。在第一部分中,我们已经对评论进行了建模,其中能缓存总投票数以及投票者ID列表。评论中的相关内容部分看起来是这样的。

{helpful_votes:3,
voter_ids:[ObjectId("4cb1476238d3b4dd5000041"),voter_ids:[ObjectId("7cb1476238d3b4dd5000042"),voter_ids:[ObjectId("7cb1256238d3b4dd5000001")]
}
可以通过针对性更新来记录用户投票。使用$push操作符将投票者的ID添加到列表中,使用$inc操作符来增加总投票数,这两个操作都在同一个更新操作里

db.reviews.update({_id:ObjectId('4c4b1476238d3b4dd5000041')},{'$push':{voter_ids:ObjectId("4c4b1476238d3b4dd8000041")},$inc:{helpful_votes:1}})
    大多数情况下这个更新没问题,但我们要需要确保仅当正在投票的用户尚未对该评论投过票时才能进行投票。因此要修改此处的查询选择器,只匹配voter_ids数组中不包含要添加ID的情况。使用$ne查询操作符就能轻松实现了。

query_selector={_id:{ObjectId("4c4b1476238d3b4dd5000041"),voter_ids:{$ne:ObjectId("4c4b1476238d3b4dd5000001")}}}
db.review.update(quert_selector,{'$push':{voter_ids:ObjectId("4c4b1476238d3b4dd5000001")},$inc:{helpful_votes:1}})
       这是一个很强大的示例,演示了MongoDB的更新机制以及如何将其用于面向文档的Schema。本例中的投票既是原子操作,又有很高的效率。原子性保证了即使在高并发环境下,也没人能投两次票。高效是因为对投票者身份的判断、更新计数器和投票者列表的操作都是在同一个服务器请求内完成的。

      现在,如果最终确定使用该技术来保存投票信息,请务必保证其他对评论文档的更新都是针对性更新,因为替换更新的方式一定会导致不一致性。想象一下,假设用户通过替换更新来修改投票内容,先要查询希望修改的文档,在查询评论和更新之间,另一个用户很有可能在为该评论投票。


很明显,T3时刻的文档替换会覆盖T2时刻发生的投票更新。使用之前描述的乐观锁技术是可以避免这种情况的,但确保本例中所有的更新都是针对性更新似乎更容易一些。







  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值