Rails 3: Как определить after_commit действие в наблюдателях? (Создание/обновление/уничтожить)
У меня есть наблюдатель, и я регистрирую обратный вызов after_commit
.
Как я могу узнать, был ли он запущен после создания или обновления?
Я могу сказать, что элемент был уничтожен, запросив item.destroyed?
, но #new_record?
не работает, поскольку элемент был сохранен.
Я собирался решить эту проблему, добавив after_create
/after_update
и сделав что-то вроде @action = :create
внутри и проверив @action
в after_commit
, но кажется, что экземпляр наблюдателя является синглом, и я могу просто переопределите значение до того, как оно дойдет до after_commit
. Поэтому я решил это уродливым способом, сохраняя действие на карте на основе item.id на after_create/update и проверяя его значение на after_commit. Действительно уродливый.
Есть ли другой способ?
Update
Как сказал @tardate, transaction_include_action?
является хорошим показателем, хотя это частный метод, и в наблюдателе к нему следует обращаться с помощью #send
.
class ProductScoreObserver < ActiveRecord::Observer
observe :product
def after_commit(product)
if product.send(:transaction_include_action?, :destroy)
...
К сожалению, опция :on
не работает в наблюдателях.
Просто убедитесь, что вы проверяете ад своих наблюдателей (ищите test_after_commit
gem, если используете use_transactional_fixtures), поэтому, когда вы переходите на новую версию Rails, вы будете знать, работает ли она еще.
(проверено в 3.2.9)
Обновление 2
Вместо наблюдателей я теперь использую ActiveSupport:: Concern и after_commit :blah, on: :create
работает там.
Ответы
Ответ 1
Я думаю, transaction_include_action? - это то, что вам нужно. Он дает достоверную информацию о конкретной транзакции в процессе (проверен в версии 3.0.8).
Формально он определяет, включала ли транзакция действие для: create,: update или: destroy. Используется для фильтрации обратных вызовов.
class Item < ActiveRecord::Base
after_commit lambda {
Rails.logger.info "transaction_include_action?(:create): #{transaction_include_action?(:create)}"
Rails.logger.info "transaction_include_action?(:destroy): #{transaction_include_action?(:destroy)}"
Rails.logger.info "transaction_include_action?(:update): #{transaction_include_action?(:update)}"
}
end
Также интересен transaction_record_state, который может быть использован для определения того, была ли запись создана или уничтожена в транзакции. Состояние должно быть одним из: new_record или: destroy.
Обновление для Rails 4
Для тех, кто хочет решить проблему в Rails 4, этот метод теперь устарел, вы должны использовать transaction_include_any_action?
, который принимает array
действий.
Пример использования:
transaction_include_any_action?([:create])
Ответ 2
Сегодня я узнал, что вы можете сделать что-то вроде этого:
after_commit :do_something, :on => :create
after_commit :do_something, :on => :update
Где do_something - это метод обратного вызова, который вы хотите вызвать для определенных действий.
Если вы хотите вызвать тот же обратный вызов для update и create, но не destroy, вы также можете использовать:
after_commit :do_something, :if => :persisted?
Это действительно не задокументировано, и мне было трудно разобраться с ним. К счастью, я знаю несколько блестящих людей. Надеюсь, это поможет!
Ответ 3
Вы можете решить, используя два метода.
-
Подход, предложенный @nathanvda, то есть проверка created_at и updated_at. Если они одинаковы, запись создается вновь, иначе это обновление.
-
Используя виртуальные атрибуты в модели. Шаги:
Ответ 4
Основываясь на идее leenasn, я создал несколько модулей, которые позволяют использовать обратные вызовы after_commit_on_update
и after_commit_on_create
: https://gist.github.com/2392664
Использование:
class User < ActiveRecord::Base
include AfterCommitCallbacks
after_commit_on_create :foo
def foo
puts "foo"
end
end
class UserObserver < ActiveRecord::Observer
def after_commit_on_create(user)
puts "foo"
end
end
Ответ 5
Взгляните на тестовый код: https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb
Здесь вы можете найти:
after_commit(:on => :create)
after_commit(:on => :update)
after_commit(:on => :destroy)
и
after_rollback(:on => :create)
after_rollback(:on => :update)
after_rollback(:on => :destroy)
Ответ 6
Мне любопытно узнать, почему вы не могли переместить логику after_commit
в after_create
и after_update
. Есть ли какое-то важное изменение состояния, которое происходит между последними 2 вызовами и after_commit
?
Если ваша обработка создания и обновления имеет некоторую перекрывающуюся логику, вы можете просто использовать последние методы 2 третьего метода, передавая действие:
# Tip: on ruby 1.9 you can use __callee__ to get the current method name, so you don't have to hardcode :create and :update.
class WidgetObserver < ActiveRecord::Observer
def after_create(rec)
# create-specific logic here...
handler(rec, :create)
# create-specific logic here...
end
def after_update(rec)
# update-specific logic here...
handler(rec, :update)
# update-specific logic here...
end
private
def handler(rec, action)
# overlapping logic
end
end
Если вы все еще предпочитаете использовать after_commit, вы можете использовать переменные потока. Это не будет утечка памяти, пока мертвые потоки могут быть собраны в мусор.
class WidgetObserver < ActiveRecord::Observer
def after_create(rec)
warn "observer: after_create"
Thread.current[:widget_observer_action] = :create
end
def after_update(rec)
warn "observer: after_update"
Thread.current[:widget_observer_action] = :update
end
# this is needed because after_commit also runs for destroy's.
def after_destroy(rec)
warn "observer: after_destroy"
Thread.current[:widget_observer_action] = :destroy
end
def after_commit(rec)
action = Thread.current[:widget_observer_action]
warn "observer: after_commit: #{action}"
ensure
Thread.current[:widget_observer_action] = nil
end
# isn't strictly necessary, but it good practice to keep the variable in a proper state.
def after_rollback(rec)
Thread.current[:widget_observer_action] = nil
end
end
Ответ 7
Это похоже на ваш первый подход, но он использует только один метод (before_save или before_validate, чтобы действительно быть в безопасности), и я не понимаю, почему это переопределит любое значение
class ItemObserver
def before_validation(item) # or before_save
@new_record = item.new_record?
end
def after_commit(item)
@new_record ? do_this : do_that
end
end
Update
Это решение не работает, поскольку, как указано в @eleano, ItemObserver является Singleton, он имеет только один экземпляр. Таким образом, если 2 элемента сохраняются одновременно, то @new_record может принимать свое значение из item_1, а after_commit запускается item_2. Чтобы преодолеть эту проблему, должно быть item.id
проверка/сопоставление для "постсинхронизации" двух методов обратного вызова: хакерство.
Ответ 8
Я использую следующий код, чтобы определить, является ли это новой записью или нет:
previous_changes[:id] && previous_changes[:id][0].nil?
На основе идеи, что новая запись имеет id по умолчанию, равную nil, а затем изменяет ее на save.
Конечно, изменение идентификатора - не обычный случай, поэтому в большинстве случаев второе условие можно опустить.
Ответ 9
Вы можете изменить привязку событий от after_commit до after_save, чтобы захватить все события создания и обновления. Затем вы можете использовать:
id_changed?
... помощник в наблюдателе. Это будет верно при создании и false при обновлении.