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

接着上篇博文,本篇继续分析更新、原子操作和删除的相关知识。

2.3 订单

     实战中对于评价文档的相关操作上看到了更新操作的原子性和高效性,在订单中也同样适用。在订单的场景中,使用针对性更新实现“添加到购物车功能(Add to cart)”。这个过程分为两步:第一步,构建一个产品文档,用来保存订单条目数组;第二步,发起一次针对性更新,表明这是一次upsert--如果要更新的文档不存在则插入一个新的文档的更新操作;如果该订单对象不存在,该操作会创建一个新的订单对象,无缝地处理初始化以及后续“添加到购物车”的动作。

第一步,构建一个要添加到购物车中的示例文档

cart_item={
_id:ObjectId("4c4b1476238d3b4dd5003981"),
slug:"Wheel-barrow-9092",
sku:"9092",
name:"Extra Large Wheel Barrow",
pricing:{
retail:589700,
sale:489700
}
}
构建该文档时,可能是查询products集合,随后抽取出需要保存为订单条目的字段,产品中的_id,sku,slug,name和pricing字段应该够了。有了购物车的明细文档,就可以把它upsert进订单集合了:

selector={user_id:ObjectId("4c4b1476238d3b4dd5000001"),
state:'CART',
'line_item.id':{'$ne':ObjectId("4c4b1476238d3b4dd5003981")}}
update={'$push':{'line_items':cart_item}}
db.orders.update(selector,update,true,false)
执行完上述两部分代码后,会想订单集合中插入一个新的订单信息文档,随后需要再发起一次针对性更新,确保明细数量和订单小计的正确性:

selector={user_id:ObjectId("4c4b1476238d3b4dd5000001"),
state:"CART",
'line_items._id':ObjectId("4c4b1476238d3b4dd5003981")
}
update={$inc:{'line_items.$.qty':1,
sub_total:cart_item['pricing']['sale']
}
}
db.orders.update(selector,update)
请注意,这里使用了$inc操作符来更新订单小计和单独条目的数量。第二条更新使用了上一节介绍的位置操作符($)。需要第二条更新的主要原因是要处理用户单击添加到购物车的东西已经存在于购物车的情况。针对这种情况,第一条更新不会成功,但仍然需要调用数量和小计。因此,在单机两次手推车的添加到购物车的时候,购物车看起来应该是下面的这样的,执行

 db.orders.find({user_id:ObjectId("4c4b1476238d3b4dd5000001")}).pretty()
pretty指令的使用是为了美化查询效果,可以以文档JSON的结构显示得到的显示结果如下:

通过结果分析发现,订单中的数量qty和金额小计sub_total的都是原始值的两倍。体现了添加了两个手推车。

3 文档原子处理

       在MongoDB的使用中,有一个指令你肯定不想错过,那就是findAndModify命令。该命令允许对文档进行原子性更新,并在同一次调用中将其返回。举例来说,可以使用findAndModify来构建任务队列和状态机,随后用这些简单的构件来实现基础的事务语义,这在极大程度上扩展了能用MongoDB构建的应用程序范围。有了这些与事务类似的特性,就能在MongoDB上构造出整个电子商务站点,不仅是产品内容,还有结账和库存管理。
        我们会通过两个实际的findAndModify命令的例子来做演示。首先展示如何处理购物车中的基本状态变迁,然后看一个更进一步的例子--管理有限的库存

3.1 订单状态的变迁

        所有状态变迁都有两个部分:一次查询,确保是一个合法的初始状态;一次更新,触发状态的变更。让我们跳过一些订单处理的步骤,假设用户正在单击“现在支付”功能按钮来授权购买。如果要在应用程序端同步授权用户的信用卡,确保以下几件事:

(1)  只能授权用户在结账界面看到的金额

(2)  在授权过程中购物车的内容不能发生变化

(3)  授权过程发生错误时,要让购物车回到前一状态

(4)  如果信用卡授权成功,将支付信息提交到订单里,订单状态变为SHIPPING_PENDING
第一步是让订单进入PRE_AUTHORIZE状态。我们使用findAndModify查找用户的当前订单对象,并确保是CART状态的:

db.orders.findAndModify({
query:{user_id:ObjectId("4c4b1476238d3b4dd5000001"),state:"CART"},
update:{"$set":{"state":"PRE_AUTHORIZE"}},
new:true
})


执行后,状态成功发生变化:


如果执行成功,findAndModify会返回状态变化后的订单对象。一旦订单进入PRE_AUTHORIZE状态,用户就无法再编辑购物车的内容了,这是因为对购物车的所有更新总是确保CART状态。现在,处于预授权状态,我们利用返回的订单对象,重新计算各项总计。计算完毕后,发出新的findAndModify,当新的总计和之前的一致时,将订单的状态变迁为AUTHORIZE。下面是第二步用到的findAndModify命令:

db.orders.findAndModify({
query:{user_id:ObjectId("4c4b1476238d3b4dd5000001"),
total:99000,
state:"PRE_AUTHORIZE"},
update:{"$set":{"state":"AUTHORIZE"}}
})
如果第二个findAndModify失败了,那么必须将订单的状态退回为CART,并将更新后的总计信息告诉用户;但如果它成功了,那么我们就知道授权的总金额和呈现给客户的金额是一样的,也可以说继续进行实际的授权API调用了。应用程序现在会对用户的信用卡发起一次信用卡授权申请。如果授权失败,和之前一样,将失败记录下来,将订单退回CART状态。

       如果授权成功,将授权信息写入订单,订单流转到下一个状态,两步都在同一个findAndModify调用里完成。下面的例子通过一个示例文档来表示接受到的授权信息,它会附加到原订单上:

auth_doc={
ts:new Date,
cc:3432003948293040,
id:292383829102384483949348,
gateway:"Authorize.net"}
db.order.findAndModify({
query:{user_id:ObjectId("4c4b1476238d3b4dd5000001"),
state:"AUTHORIZING"},
update:{"$set":{"state":"PRE-SHIPPPING"},"authorization":auth_doc}
})
     请注意,MongoDB的一些特性简化了这个事务性过程。我们可以原子性地修改任意文档,单个连接中能保证读取的一致性。最后,文档结构本身也允许这些操作来适应MongoDB提供的单文档原子性。下面的章节将讲解多对多的事务行为。

3.2 库存管理

    并非所有的网站都有库存管理,大多数商品都有充足的时间进货,这使得订单不用考虑当前商品的实际数量。这种情况下,库存管理就是简单地管理期望值;当库存仅有少量存货时,调整送货预期即可。

    限量商品则有不同的挑战。假设正在销售指定座位的音乐会门票或者手工艺术品,这些铲平是不能套期保值的,用户总是希望能够买到自己所选的产品。本部分我们将通过MongoDB的一套可行性解决方案。这能进一步说明findAndModify命令的创造性,以及如何明智地使用文档模型,还能演示如何跨多个文档的事务性语义。

    建模库存的最好方法就是想象一个真实的商店。如果在一家园艺商店里,我们能看见并感受到实际库存量;很多铲子、耙子和剪刀在过道里摆成一排。要是我们拿起一把铲子放进购物车里,对其他顾客而言就少了一把铲子,其结果就是两个客户不能同时在他们的购物车里拥有同一把铲子。我们可以使用这个简单的原则来建立库存。在库存集合中为仓库里的每个实际商品保存一个对应的文档。如果仓库有10把铲子,数据库里就有10个铲子文档。每个库存项都通过sku链接到产品上,并且拥有AVAILABLE(0),IN_CART(1),PRE_ORDER(2)和PURCHASED(3)这四个状态中的某个状态。

下面的代码分别插入三把铲子,三把耙子和三把剪子

3.times do
     inventory.insert_one({:sku=>'shovel',:state=>"AVAILABLE"})
     inventory.insert_one({:sku=>'rake',:state=>"AVAILABLE"})
     inventory.insert_one({:sku=>'clippers',:state=>"AVAILABLE"})
end
插入结果如下:


    我们将用一个特殊的库存获取来管理库存。我们先看看它是如何工作的,然后深入其中,揭示它的实现原理。库存获取器能向购物车内添加任意产品集合。此处,我们创建了一个新订单对象与一个新的库存获取器。随后获取器向指定订单添加了三把铲子和一把剪刀,订单由add_to_cart方法的订单ID指定,另外再传入两个文档指定产品和数量

order_id=order.insert({:username=>'kabnker',:item_ids=[]})
fetcher=InventoryFetcher.new(:orders=>orders,:inventory=>inventory)
fecther.add_to_cart(order_id,{:sku=>"shovel",:qty=>3},{:sku=>"clippers",:qty=>1})
order=orders.find_one({"_id"=>order_id})
puts "\nHere's the order:"
p order
    针对关键的InventoryFetcher,我们不会详细讲解每行代码,但是我们会讲解三个主要的方法。首先当传入一个要添加到购物车里的商品列表时,库存获取器会尝试将它们的状态从AVALIABLE变更为IN_CART。如果操作中有哪一步失败(比如某项商品未能添加到购物车里),那么整个操作就会回滚。看看之前调用的add_to_cart方法:

def add_to_cart(order_id,*items)
    item_selectors=[]
    items.each do |item|
       item[:qty].timies do
            item_selectors <<{:sku=>item[:sku]}
       end
     end
    transition_state(order_id,item_selectors,:from=>AVAILABLE,:to=>IN_CART)
end
该方法并没有完成上述功能,它只是接收要添加到购物车的具体商品并增加其数量,这样没见实际添加到购物车里的商品都能有一个库存项选择器。举例来说,以下文档表示想添加两把铲子

{:sku=>"shovel",:qty=>2}

会变成[{:sku=>"shovel"},{:sku=>"shovel"}]

     针对每件要添加到购物车里的商品,都需要一个单独的查询选择器。因此,add_to_cart方法会将库存项选择器数组传给一个名为transition_state的方法。例如上述代码指明了状态应该从AVALABLE变更为IN_CART:

def transition_state(order_id,selectors,opts={})
    item_transitioned=[]
    begin
      for selector in selctors do
          query=selector.merge(:state=>opts[:from])
          physical_item=inventory.find_and_modify(:query=query.:update=>{'$set'=>{:state=>opts[:to].:ts=>Time.now.utc}})
          if physical_item.nil?
              raise InventoryFetchFaliure
          end
          items_transitioned << physical_item['_id']
          orders.update({:_id=>order_id},{"$push"=>{:item_ids=>physical_item['_id']}})
      end
      rescue Mongo::OperationFaliure,InventoryFetchFaliure
       rollback(order_id,item_transitioned,opts[:from],opts[:to])
       raise InventoryFetchFaliure,"Failed to add #{selectors[:sku]}"
    end
  items_transitioned.size
end
  为了变更状态,每个选择器都有一个额外的条件(:state=>AVALIABLE),随后选择器会被传给findAndModify,如果条件匹配,则设置时间戳和库存项的新状态。transition_state方法会保存变更过状态的库存项列表,将它们的ID更新到订单里。

   如果findAndModify命令执行失败并返回nil,那么会抛出一个InventoryFetchFailure异常。如果命令由于网络错误而失败,那么必然会有Mongo::OperationFailure异常,我们需要捕获该异常。这两种情况下,都要回滚之前修改过的库存项,随后抛出一个InventoryFetchFailure,其中包含了无法添加的库存项SKU。随后能在应用层捕获该异常,告诉用户操作失败。下面是回滚部分的代码

def rollback(order_id,item_ids,old_state,new_state)
   orders.update({"_id"=>order_id},{"$pullAll"=>{item_ids=>item_ids}})
    item_ids.each do |id|
        inventory.find_and_modify(:query=>{"_id"=>id.:state=>new_state},:update=>{"$set"=>{:state=>old_state,:ts=Time.now.utc}})
    end
end
我们使用$pullAll操作符删除了刚才添加到订单item_ids数组里的所有ID。然后遍历库存项ID列表,将每项的状态改回原来的样子。可以将transition_state方法作为其他变更库存状态方法的基础,要将其整合进在上一节构建的订单流转系统中并不困难。

关于MongoDB的更新和删除的具体细节会在下一部分继续分析


   



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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值