Rails 3: How to identify after_commit action in observers? (create/update/destroy)

前端 未结 9 1296
慢半拍i
慢半拍i 2021-01-30 02:17

I have an observer and I register an after_commit callback. How can I tell whether it was fired after create or update? I can tell an item was destroyed by asking

相关标签:
9条回答
  • 2021-01-30 02:40

    This is similar to your 1st approach but it only uses one method (before_save or before_validate to really be safe) and I don't see why this would override any value

    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

    This solution doesn't work because as stated by @eleano, ItemObserver is a Singleton, it has only one instance. So if 2 Item are saved at the same time @new_record could take its value from item_1 while after_commit is triggered by item_2. To overcome this problem there should be an item.id checking/mapping to "post-synchornize" the 2 callback methods : hackish.

    0 讨论(0)
  • 2021-01-30 02:41

    I'm curious to know why you couldn't move your after_commit logic into after_create and after_update. Is there some important state change that happens between the latter 2 calls and after_commit?

    If your create and update handling has some overlapping logic, you could just have the latter 2 methods call a third method, passing in the action:

    # 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
    

    If you still rather use after_commit, you can use thread variables. This won't leak memory as long as dead threads are allowed to be garbage-collected.

    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
    
    0 讨论(0)
  • 2021-01-30 02:42

    I think transaction_include_action? is what you are after. It gives a reliable indication of the specific transaction in process (verified in 3.0.8).

    Formally, it determines if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.

    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
    

    Also of interest may be transaction_record_state which can be used to determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed.

    Update for Rails 4

    For those seeking to solve the problem in Rails 4, this method is now deprecated, you should use transaction_include_any_action? which accepts an array of actions.

    Usage Example:

    transaction_include_any_action?([:create])
    
    0 讨论(0)
  • 2021-01-30 02:45

    I use the following code to determine whether it is a new record or not:

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

    It based on idea that a new record has default id equal to nil and then changes it on save. Of course id changing is not a common case, so in most cases the second condition can be omitted.

    0 讨论(0)
  • 2021-01-30 02:45

    You can change your event hook from after_commit to after_save, to capture all create and update events. You can then use:

    id_changed?
    

    ...helper in the observer. This will be true on create and false on an update.

    0 讨论(0)
  • 2021-01-30 02:47

    You can solve by using two techniques.

    • The approach suggested by @nathanvda i.e. checking the created_at and updated_at. If they are same, the record is newly created, else its an update.

    • By using virtual attributes in the model. Steps are:

      • Add a field in the model with the code attr_accessor newly_created
      • Update the same in the before_create and before_update callbacks as

        def before_create (record)
            record.newly_created = true
        end
        
        def before_update (record)
            record.newly_created = false
        end
        
    0 讨论(0)
提交回复
热议问题