Ruby: как обработать оптимистическую блокировку с помощью update_all (атрибуты) - PullRequest
0 голосов
/ 03 октября 2018

Я пытаюсь реализовать Оптимистическую блокировку для состояния гонки.Для этого я добавил дополнительный столбец 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 голосов
/ 03 октября 2018

Возможное решение:

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 выше STILL НЕ ГАРАНТИРУЕТ, что условия гонки будут избегаться!потому что см. пример ниже:

    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 выше будет работать только для "вычислений / зависимостей"в ваших обратных вызовах модели, но не будет работать должным образом, если у вас есть вычисления / зависимости вне обратных вызовов вашей модели 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 (см. документацию) , чтобы вы могли его специально спасти,и решить, что делать соответственно, если произошло это состояние гонки.

...