Рельсы 3:Как определить после совершения действия в наблюдателях? (создать / обновить / уничтожить)


у меня есть наблюдатель, и я регистрирую after_commit обратный. Как я могу сказать, был ли он уволен после создания или обновления? Я могу сказать, что предмет был уничтожен, спросив item.destroyed? но #new_record? не работает с момента сохранения элемента.

я собирался решить ее, добавив after_create/after_update и сделать что-то вроде @action = :create внутри и проверьте @action at after_commit, но кажется, что экземпляр observer является одноэлементным, и я мог бы просто переопределить значение, прежде чем он доберется до after_commit. Поэтому я решил его более уродливым способом, сохранив действие на карте на основе item.id на after_create / update и проверка его значения на after_commit. Очень некрасиво.

есть ли другой способ?

обновление

как сказал @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 там работает.

9 57

9 ответов:

Я думаю 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 или уничтожены.

обновление для Rails 4

для тех, кто стремится решить проблему в Rails 4, Этот метод является теперь устаревшим, вы должны использовать transaction_include_any_action?, которая принимает array действий.

Пример Использования:

transaction_include_any_action?([:create])

Я сегодня узнал, что вы можете сделать что-то вроде этого:

after_commit :do_something, :on => :create

after_commit :do_something, :on => :update

здесь выполнить_действие - это метод обратного вызова, который вы хотите звонить на определенные действия.

Если вы хотите вызвать тот же обратный вызов для обновление и создать, но не уничтожить, вы также можете использовать: after_commit :do_something, :if => :persisted?

это действительно не задокументировано хорошо, и мне было трудно гуглить его. К счастью, я знаю несколько прекрасных людей. Надеюсь на это помогает!

вы можете решить с помощью двух методов.

  • подход, предложенный @nathanvda, т. е. проверка created_at и updated_at. Если они одинаковы, запись создается заново, иначе ее обновление.

  • С помощью виртуальных атрибутов в модели. Шаги:

    • добавить поле в модель с кодом attr_accessor newly_created
    • обновить то же самое в before_create и before_update callbacks как

      def before_create (record)
          record.newly_created = true
      end
      
      def before_update (record)
          record.newly_created = false
      end
      

основываясь на идее 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

взгляните на тестовый код: 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)

мне любопытно знать, почему вы не могли переместить свой 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's good practice to keep the variable in a proper state.
  def after_rollback(rec)
    Thread.current[:widget_observer_action] = nil
  end
end

Это похоже на ваш 1-й подход, но он использует только один метод (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

обновление

это решение не работает, потому что, как указано @eleano, ItemObserver является Одноэлементным, он имеет только один экземпляр. Так что если 2 элемента сохраняются в то же время @new_record может принять его значение из item_1 в то время как after_commit запускается item_2. Чтобы преодолеть эту проблему есть должно быть item.id проверка / сопоставление с "постсинхронизацией" 2 метода обратного вызова : hackish.

Я использую следующий код, чтобы определить, является ли это новый рекорд или нет:

previous_changes[:id] && previous_changes[:id][0].nil?

Он основан на идее, что новая запись имеет идентификатор по умолчанию, равный нулю, а затем изменяет его при сохранении. Конечно, изменение идентификатора не является распространенным случаем, поэтому в большинстве случаев второе условие может быть опущено.

вы можете изменить свой крюк событий с after_commit на after_save, чтобы захватить все события создания и обновления. Затем вы можете использовать:

id_changed?

...помощник в наблюдателе. Это будет верно при создании и ложно при обновлении.