57

Ho un osservatore e registro un callback after_commit. Come posso sapere se è stato attivato dopo la creazione o l'aggiornamento? Posso dire che un articolo è stato distrutto chiedendo item.destroyed? ma #new_record? non funziona poiché l'elemento è stato salvato.Rails 3: Come identificare l'azione after_commit negli osservatori? (crea/aggiorna/distruggi)

stavo per risolverlo aggiungendo after_create/after_update e fare qualcosa di simile @action = :create all'interno e controllare il @action a after_commit, ma sembra che l'istanza osservatore è un Singleton e potrei ignorare un valore prima che arrivi al after_commit. Così l'ho risolto in modo più brutto, memorizzando l'azione in una mappa basata su item.id su after_create/update e controllandone il valore su after_commit. Davvero brutto.

C'è qualche altro modo?

Aggiornamento

Come @tardate detto, transaction_include_action? è una buona indicazione, anche se è un metodo privato, e in un osservatore dovrebbe essere letta con #send.

class ProductScoreObserver < ActiveRecord::Observer 
    observe :product 

    def after_commit(product) 
    if product.send(:transaction_include_action?, :destroy) 
     ... 

Purtroppo, l'opzione :on non funziona in osservatori.

Assicurati di testare l'inferno dei tuoi osservatori (cerca test_after_commit gem se usi use_transactional_fixtures) così quando esegui l'aggiornamento alla nuova versione di Rails saprai se funziona ancora.

(testato su 3.2.9)

Update 2

al posto degli osservatori io uso ActiveSupport :: Preoccupazione e after_commit :blah, on: :create vi lavora.

+0

Stai cercando di sapere se il tuo record era nuovo o no quando il after_commit è stato attivato? Leggendo la tua domanda e le risposte, trovo che sia confusa. Potresti riformulare la frase o fornirci un chiaro esempio? – charlysisto

+0

La soluzione iniziale funziona se si utilizza un server a thread singolo. Se non lo stai utilizzando, passa a uno, ad esempio unicorno, per risolvere questo problema in modo pulito. Rende il modello di programmazione molto più semplice, avrai meno mal di testa (come questo) e alla fine sarà più veloce. L'uso di + transaction_include_action? + Non è pulito, in quanto si tratta di un metodo di guide protetto non supportato non supportato da alcun test nella suite di test delle rotaie. La prossima versione potrebbe non avere quel metodo. –

+0

@elado Sono confuso. La risposta accettata (tardate) non funziona con gli osservatori (come notato dal commento di Ches). Hai deciso di utilizzare invece i callback? Per favore aggiungi una spiegazione alla tua domanda. – Kelvin

risposta

50

Penso che transaction_include_action? sia quello che cerchi. Fornisce un'indicazione affidabile della transazione specifica in corso (verificata in 3.0.8).

Formalmente, determina se una transazione include un'azione per: create,: update o: destroy. Utilizzato nel filtrare i callback.

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 

Inoltre di interesse può essere transaction_record_state che può essere utilizzato per determinare se un record è stato creato o distrutto in una transazione. Lo stato dovrebbe essere uno di: new_record o: destroyed.

Aggiornamento per Rails 4

Per coloro che cercano di risolvere il problema in Rails 4, questo metodo è ormai deprecato, è necessario utilizzare transaction_include_any_action? che accetta un array di azioni.

Utilizzo Esempio:

transaction_include_any_action?([:create]) 
+2

Fantastico. Questo è il modo più pulito per fare ciò che ho visto. In altri casi ho utilizzato la soluzione proposta da @charlysisto (che funziona), ma ciò è più gradevole. Lo proverò. –

+0

Va notato che entrambi questi metodi sono protetti, quindi se vuoi chiamarli in un osservatore devi chiamare ': send' sull'oggetto del modello con il metodo in questione. – nirvdrum

+3

Non utilizzare questo con le nuove versioni di binari, utilizzare i seguenti metodi: È possibile eseguire 'after_commit: method_name, su:: create' –

-4

È possibile modificare il proprio hook di evento da after_commit a after_save, per acquisire tutti gli eventi di creazione e aggiornamento. È quindi possibile utilizzare:

id_changed? 

... helper nell'osservatore. Questo sarà vero su create e false su un aggiornamento.

+2

È vero in 'after_create', ma falso in' after_commit' (sia per la creazione che per l'aggiornamento). – elado

+0

Aggiornato. Usa l'evento after_save per catturare sia la creazione/l'aggiornamento sia la possibilità di distinguere tra i due. – Winfield

+1

Ma 'after_save' è all'interno della transazione. Ho bisogno di eseguire il codice al di fuori di esso, quindi, l'uso di 'after_commit'. – elado

2

Date un'occhiata al codice di prova: https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb

Vi si possono trovare:

after_commit(:on => :create) 
after_commit(:on => :update) 
after_commit(:on => :destroy) 

e

after_rollback(:on => :create) 
after_rollback(:on => :update) 
after_rollback(:on => :destroy) 
+0

Grazie, ma non funziona. '# ' quando eseguo 'after_commit (: on =>: create) {| record | mette "XXX"} 'nell'osservatore (potrebbe funzionare su un activerecord ma io uso un osservatore qui). – elado

+2

Funziona nelle versioni successive delle guide. Si può fare 'after_commit: method_name, on:: create' –

-1

Questo è simile al tuo primo approccio ma utilizza un solo metodo (before_save o before_validate per essere davvero al sicuro) e non vedo perché questo dovrebbe prevalere su qualsiasi valore

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 

Aggiornamento

Questa soluzione non funziona perché come affermato da @eleano, ItemObserver è un Singleton, che ha una sola istanza. Quindi se 2 Item vengono salvati allo stesso tempo @new_record potrebbe prendere il suo valore da item_1 mentre after_commit viene attivato da item_2.Per superare questo problema ci dovrebbe essere un controllo/mappatura item.id per "post-synchornize" i 2 metodi di callback: hackish.

+0

Questo non funzionerebbe perché l'istanza dell'osservatore è un singleton. Significato, le variabili @ sono condivise per tutti i record. Se più record sono gestiti dallo stesso osservatore, i valori non saranno corretti. Ecco perché ho creato una mappa di ID e azioni. – elado

+0

sì capisco cosa volevi dire nella domanda ora che posso vederlo. Aggiornato di conseguenza la risposta. Impari dai tuoi errori ... – charlysisto

7

è possibile risolvere utilizzando due tecniche.

  • L'approccio proposto dalla @nathanvda cioè controllando il created_at e updated_at. Se sono uguali, il record è stato appena creato, altrimenti è un aggiornamento.

  • Utilizzando gli attributi virtuali nel modello. I passaggi sono:

    • Aggiungi un campo nel modello con il codice attr_accessor newly_created
    • Aggiornare lo stesso nel before_create e before_update callbacks come

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

Sulla base di un'idea leenasn, Ho creato alcuni moduli che consentono di utilizzare i callback after_commit_on_update e after_commit_on_create: https://gist.github.com/2392664

Usage:

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

Perché un downvote per questo? Questo codice funziona senza problemi nella mia app, lo trovo abbastanza utile ... – lacco

53

ho imparato oggi che si può fare qualcosa di simile:

after_commit :do_something, :on => :create 

after_commit :do_something, :on => :update 

Dove fai_qualcosa è il metodo di callback che si desidera chiamare su determinate azioni.

Se si desidera chiamare lo stesso callback per aggiornamento e creare, ma non distruggere, è anche possibile utilizzare: after_commit :do_something, :if => :persisted?

in realtà non è documentato bene e ho avuto un momento difficile Googling esso. Fortunatamente, conosco alcune persone brillanti. Spero che sia d'aiuto!

+0

confermare lavorando su binari 3.1.0 – sai

+2

Questa dovrebbe essere la risposta accettata. –

+6

Questo non funzionerà con gli osservatori, come indicato nella descrizione della domanda –

0

Sono curioso di sapere il motivo per cui non si poteva spostare il after_commit logica in after_create e after_update. C'è qualche importante cambiamento di stato che si verifica tra le ultime 2 chiamate e after_commit?

Se la creazione e la gestione di aggiornamento ha una certa logica di sovrapposizione, si può solo avere gli ultimi 2 metodi chiamano un terzo metodo, passando all'azione:

# 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 

Se ancora piuttosto usa after_commit, è possibile utilizzare variabili del thread. Questo non perderà memoria finché i thread morti possono essere raccolti dalla garbage collection.

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

Prestazioni e blocco DB. Se avessi usato after_create/destroy avrebbe reso la transazione di wrapping più lunga, con cose per le quali non avevo bisogno di transazioni. – elado

-1

Io uso il seguente codice per determinare se si tratta di un nuovo record o no:

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

Si basa su un'idea che un nuovo record ha ID predefinito uguale a zero e poi cambia su Salva . Ovviamente la modifica dell'ID non è un caso comune, quindi nella maggior parte dei casi la seconda condizione può essere omessa.