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

时间:2018-10-03 10:18:15

标签: ruby-on-rails ruby ruby-on-rails-3 ruby-on-rails-4 race-condition

我正在尝试对比赛条件实施乐观锁定。为此,我在“产品:通过迁移进行建模”中添加了额外的列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。在上述情况下,如何停止回叫的无限循环并处理乐观锁定。

1 个答案:

答案 0 :(得分:0)

可能的解决方案:

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(甚至任何后续回调:{{1 }}或*_update),因为您可能有一个或两个后续的回调,这些回调取决于属性的当前状态(即,它正在执行一些计算或检查某些boolean-flag属性),然后您需要先重新加载(如上所述),然后再执行这些计算。

  • 上面的*_save仅适用于模型回调中的“计算/依赖关系”,但如果您的模型before_validation回调之外具有计算/依赖关系,则无法正常工作;也就是说,如果您有类似的东西:

    Product

上面的注释是为什么默认情况下,Rails不会自动重载该属性为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 之类的原因,因为根据您的应用程序/业务逻辑,您可能想“重载”或“不重载” -reload”,这就是为什么默认情况下,Rails会引发before_validation (see docs)以便您专门营救,并处理发生这种竞争情况时应采取的相应措施。