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