У нас есть приложение Rails 6.1, выполняющее обратный вызов after_commit для проверки значения saved_changes для запуска некоторых фоновых рабочих процессов. Но проблема в том, что saved_changes возвращает пустое значение, если модель имеет accepts_nested_attributes_for. Это не только after_commit другие обратные вызовы, такие как before_save, но и around_save возвращающее пустое saved_changes значение.
Например.
на модели/profile.rb
after_commit :run_worker, on: :update
belongs_to :user
accepts_nested_attributes_for :user
В данном случае saved_changes — это {}, но если я удалю accepts_nested_attributes_for, он будет работать нормально и даст мне измененное значение.
Это известная проблема, вызванная двойным сохранением, как упоминалось в этой проблеме с GH. Обходной путь для версии 7.1 — добавление конфигурации в application.rb
config.active_record.run_commit_callbacks_on_first_saved_instances_in_transaction = true
Это значение по умолчанию предназначено для Rails 7.1 и не работает для версии 6.1. Кто-нибудь еще сталкивался с этой проблемой и имеет обходной путь?





Не только after_commit другие обратные вызовы, такие как
before_save, но иaround_saveтакже возвращают пустое значение save_changes.
saved_change присутствует только после сохранения записи, поэтому ожидается, что она пуста в обратном вызове before_save, и она должна быть пустой и в обратном вызове around_save, пока запись не будет сохранена там.
Перед сохранением записи грязное состояние сохраняется в объекте changes (а не saved_changes).
Чтобы решить вашу проблему, я бы предложил сохранить changes в пользовательском атрибуте, чтобы использовать его позже в обратных вызовах after_save/after_commit. Что-то вроде этого:
attr_accessor :my_saved_changes
before_save :save_dirty_state
def save_dirty_state
self.my_saved_changes = changes
end
ОБНОВЛЕННОЕ ПРЕДЛОЖЕНИЕ С учетом комментария ниже:
Если я правильно понимаю, эти 2 сохранения происходят внутри одной транзакции, поэтому обратный вызов after_commit будет вызван только один раз. Вероятно, вы можете положиться на это и установить атрибут my_saved_changes из моего предложения только в том случае, если он еще не установлен (а затем сбросить его в обратном вызове after_commit):
attr_accessor :my_saved_changes
before_save :save_dirty_state
# Have this callback to execute the last:
after_commit :reset_dirty_state
def save_dirty_state
# Note the change of assignment here ('||=' operator instead of '=')
self.my_saved_changes ||= changes
end
def reset_dirty_state
self.my_saved_changes = nil
end
Понятно, я обновил свое предложение
Спасибо GProst, ответ работает как шарм.
Полагаясь на @GProst Ответ при обращении к вашему комментарию, похоже, вы могли бы взломать это вместе как:
after_commit :preserve_changes
attr_writer :my_saved_changes
# just so #my_saved_changes always returns a Hash
def my_saved_changes
@my_saved_changes ||= {}
end
def preserve_changes
self.my_saved_changes = saved_changes if saved_changes?
end
Это позволит избежать перезаписи вторичного сохранения, поскольку изменений для сохранения нет, поэтому saved_changes? будет ложным.
Если вы хотите использовать before_save, как предложено в указанном сообщении, вы можете вместо этого использовать has_changes_to_save? в качестве защиты.
Спасибо за ответ @GProst, но это решение не работает. Rails дважды вызывает
before_save. Первый вызов назначил «грязные» изменения, но второй вызов сбросил все ранее назначенные «грязные» изменения. Посмотрев дальше, он обнаружил подобные проблемы, возникающие и у других людей, и это похоже на ошибку Rails.