57

我有一個觀察者,我註冊了一個after_commit回調。 如何判斷在創建或更新後是否被解僱? 我可以通過詢問item.destroyed?來告訴某件物品被毀壞,但#new_record?自該物品被保存後不起作用。Rails 3:如何識別觀察者中的after_commit動作? (創建/更新/銷燬)

我正要加入after_create/after_update解決它,這樣做@action = :create內,並檢查@actionafter_commit,但似乎觀察者實例是單身,我可能只覆蓋一個值它到達之前after_commit。所以我以一種更醜陋的方式解決了這個問題,將action放在基於after_create/update上的item.id的映射中,並在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選項不適用於觀察者。

只要確保你測試了觀察者的地獄(如果你使用use_transactional_fixtures,查找test_after_commit gem),所以當你升級到新的Rails版本時,你會知道它是否仍然有效。

(測試3.2.9)

更新2

相反觀察員我現在請使用ActiveSupport ::關注和after_commit :blah, on: :create那裏工作。

+0

當after_commit被解僱時,你是否想知道你的記錄是否是新的?重新閱讀你的問題和答案,我覺得很困惑。你可以重述一下嗎?或者給我們一個清晰的例子嗎? – charlysisto

+0

如果您使用單線程服務器,則您的初始解決方案可以正常工作。如果你不使用它,然後切換到一個,如獨角獸,這將以一種乾淨的方式解決這個問題。它使得編程模型變得容易得多,你將會減少頭痛(像這樣),最終它會更快。使用+ transaction_include_action?+不是乾淨的,因爲它是一個不受rails測試套件中任何測試支持的不受支持的受保護的rails方法。下一個版本可能沒有這種方法。 –

+0

@elado我很困惑。接受的答案(tardate's)不適用於觀察者(正如ches的評論所指出的那樣)。你是否轉而使用回調呢?請附上對您的問題的解釋。 – Kelvin

回答

50

我認爲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

對於那些尋求解決軌道4,5的問題,這種方法現在已經過時,你應該使用transaction_include_any_action?它接受的動作的array

使用例:

transaction_include_any_action?([:create]) 
+2

太棒了。這是我見過的最乾淨的方法。在其他情況下,我使用了@charlysisto提供的解決方案(它可以工作),但是這樣感覺更好。我會試試這個。 –

+0

應該注意的是,這兩種方法都受到保護,所以如果您想在觀察者中調用它們,則必須使用所討論的方法在模型對象上調用':send'。 – nirvdrum

+3

不要在rails的新版本中使用這個,使用下面的方法: 你可以使用'after_commit:method_name,on :: create' –

-4

您可以將事件掛鉤從after_commit更改爲after_save,以捕獲所有創建和更新事件。您可以使用:

id_changed? 

...在觀察員中的助手。這在創建時是成立的,在更新時是錯誤的。

+2

'after_create'確實如此,'after_commit'則是false(對於創建和更新)。 – elado

+0

已更新。使用after_save事件來捕獲創建/更新並能夠區分這兩者。 – Winfield

+1

但是'after_save'在事務中。我需要在它之外執行代碼,因此,使用'after_commit'。 – elado

2

看看測試代碼: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) 
+0

謝謝,但它不起作用。 '#'當我做'after_commit(:on =>:create){| record |在觀察者中放置「XXX」}'(它可能正在處理activerecord,但我在這裏使用觀察者)。 – elado

+2

它可以在更高版本的rails中使用。你可以這樣做'after_commit:method_name,on::create' –

-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是一個Singleton,它只有一個實例。因此,如果同時保存2個項目,@ new_record可以從item_1中取值,而after_commit由item_2觸發。爲了克服這個問題,應該有一個item.id檢查/映射到2個回調方法的「後同步」:hackish。

+0

這是行不通的,因爲觀察者的實例是一個單例。含義,所有記錄共享@變量。如果多個記錄由同一個觀察者處理,則這些值將不正確。這就是爲什麼我創建了ID和動作的地圖。 – elado

+0

是的我明白你在問題中的意思,我可以看到它。相應地更新了答案。你從你的錯誤中學習... – charlysisto

7

可以通過使用兩種技術解決。

  • @nathanvda建議的方法,即檢查created_at和updated_at。如果它們相同,則記錄是新創建的,否則是其更新。

  • 通過在模型中使用虛擬屬性。步驟是:

    • 在模型中before_createbefore_update callbacks基於leenasn的想法添加一個字段代碼attr_accessor newly_created
    • 更新一樣

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

,我創建了一些模塊,可以使用after_commit_on_updateafter_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 
+0

爲什麼要這樣做?此代碼在我的應用程序中沒有問題,我發現它非常有用... – lacco

53

我今天瞭解到,你可以做這樣的事情:

after_commit :do_something, :on => :create 

after_commit :do_something, :on => :update 

do_something是要在某些行動調用回調方法。

如果你想呼籲更新相同的回調創建,但不破壞,你也可以使用: after_commit :do_something, :if => :persisted?

這真的不是記錄很好,我的日子不好過谷歌搜索它。幸運的是,我認識一些有才華的人。希望能幫助到你!

+0

確認在rails上工作3.1.0 – sai

+2

這應該是公認的答案。 –

+6

這將不會與觀察員一起工作,如問題描述中所述 –

0

我很想知道爲什麼你不能將after_commit邏輯移動到after_createafter_update。在後兩個電話和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 
+2

性能和數據庫鎖定。如果我使用after_create/destroy,它會使包裝事務處理時間延長,而我不需要事務處理。 – elado

-1

我用下面的代碼,以確定它是否是一個新的記錄或不:

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

它基於想法,一個新的記錄具有默認的ID等於零,然後改變它保存。 當然id變化不是一個常見的情況,所以在大多數情況下,第二個條件可以省略。