Ruby:如何使用update_all(属性)处理乐观锁定

我正在尝试为种族条件实施乐观锁定。 为此,我在Product:Model中通过迁移添加了额外的列lock_version

 #Product: Model's new field: # attribute_1 # lock_version :integer(4) default(0), not null before_validation :method_1, :if => :recalculation_required_attribute def method_1 #### #### if self.lock_version == Product.find(self.id).lock_version Product.where(:id => self.id).update_all(attributes) self.attributes = attributes self.save! end end 

产品型号具有attribute_1 。 如果attribute_1需要重新计算,则before_validation: method_1将调用。

我使用lock_version使用乐观锁定。 但是, update_all不会增加lock_version 。 所以我开始使用save! 。 现在我收到一个新错误: SystemStackError: stack level too deep因为self.save! 触发before_validation: method1 。 如何在上述情况下停止回调的无限循环并处理乐观锁定。

可能解决方案

 class Product < ApplicationRecord before_validation :reload_and_apply_changes_if_stale, on: :update def reload_and_assign_changes_if_stale # if stale if lock_version != Post.find(id).lock_version # store the "changes" first into a backup variable current_changes = changes # reload this record from "real" up-to-date values from DB (because we already know that it's stale) reload # after reloading, `changes` now becomes `{}`, and is why we need the backup variable `current_changes` above # now finally, assign back again all the "changed" values current_changes.each do |attribute_name, change| change_from = change[0] # you can remove this line change_to = change[1] self[attribute_name] = change_to end end end end 

重要笔记:

  • 上面的before_validation仍然不保证将避免竞争条件! 因为看下面的例子:

     class Product < ApplicationRecord # this triggers first... before_validation :reload_and_apply_changes_if_stale, on: :update # then, this triggers next... before_update :do_some_heavy_loooong_calculation def do_some_heavy_loooong_calculation sleep(60.seconds) # ... of which during this time, this record might already be stale! as perhaps another "process" or another "server" has already updated this record! end 
  • 确保上面的before_validation位于Post模型的最顶层,以便在任何其他before_validations(甚至任何后续回调: *_update*_save )之前首先触发该回调,因为您可能有一个或两个后续回调依赖于属性的当前状态(即它正在进行一些计算,或检查一些boolean-flag属性),然后在进行这些计算之前需要先重新加载(如上所述)。

  • 上面的before_validation仅适用于模型回调中的“calculate / dependencies”,但如果您在Product模型的回调之外有计算/依赖项,则无法正常工作; 即如果你有类似的东西:

     class ProductsController < ApplicationController def update @product = Product.find(params[:id]) # let's assume at this line, @product.cost = nil (no value yet) @product.assign_attributes(product_attributes) # let's assume at this line, @product.cost = 1100 # because 1100 > 1000, then DO SOME IMPORTANT THING! if @product.cost_was.nil? && @product.cost > 1_000.0 # do some important thing! end # however, when `product.save` is called below and the `before_validation :reload_and_apply_changes_if_stale` is triggered, # of which let's say some other "process" has already updated this # exact same record, and thus @product is reloaded, but the real DB value is now # @product.cost = 900; there's no WAY TO UNDO SOME IMPORTANT THING! above @product.save end end 

上面的注释是为什么默认情况下Rails不会将这些属性自动重新加载为before_validation或其他东西,因为根据您的应用程序/业务逻辑,您可能想要“重新加载”或“不重新加载”,这就是为什么默认情况下Rails会引发一个ActiveRecord::StaleObjectError (请参阅docs),以便您专门进行救援,并在发生这种竞争条件时处理相应的操作。

Interesting Posts