update software rails official last ruby-on-rails ruby-on-rails-3 transactions observer-pattern

ruby on rails - software - Rieles 3: ¿Cómo identificar la acción after_commit en los observadores?(crear/actualizar/destruir)



ruby on rails website (9)

Tengo un observador y registro una after_commit llamada after_commit . ¿Cómo puedo saber si se activó después de crear o actualizar? Puedo decir que un artículo fue destruido al preguntar item.destroyed? pero #new_record? no funciona ya que el artículo fue guardado.

Iba a resolverlo agregando after_create / after_update y hacer algo como @action = :create inside y comprobar @action en after_commit , pero parece que la instancia del observador es un singleton y podría anular un valor antes de que llegue a el after_commit Así que lo resolví de una manera más fea, almacenando la acción en un mapa basado en el elemento.id en after_create / update y comprobando su valor en after_commit. Realmente feo.

¿Hay alguna otra manera?

Actualizar

Como dijo @tardate, transaction_include_action? es una buena indicación, aunque es un método privado, y en un observador se debe acceder con #send .

class ProductScoreObserver < ActiveRecord::Observer observe :product def after_commit(product) if product.send(:transaction_include_action?, :destroy) ...

Lamentablemente, la opción :on no funciona en los observadores.

Solo asegúrate de probar el demonio de tus observadores (busca la test_after_commit si usas use_transactional_fixtures) para que cuando actualices a la nueva versión de Rails sabrás si todavía funciona.

(Probado en 3.2.9)

Actualización 2

En lugar de Observadores ahora uso ActiveSupport :: Concern y after_commit :blah, on: :create funciona allí.


Basado en la idea leenasn, creé algunos módulos que hacen posible usar after_commit_on_update y after_commit_on_create callbacks: https://gist.github.com/2392664

Uso:

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


Creo que transaction_include_action? es lo que buscas Da una indicación confiable de la transacción específica en proceso (verificado en 3.0.8).

Formalmente, determina si una transacción incluye una acción para: crear, actualizar o destruir. Usado para filtrar devoluciones de llamada.

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

También puede ser de interés transaction_record_state que se puede usar para determinar si se creó o destruyó un registro en una transacción. State debe ser uno de: new_record o: destroyed.

Actualización para Rails 4

Para aquellos que buscan resolver el problema en Rails 4, este método ahora está en desuso, debe usar transaction_include_any_action? que acepta una array de acciones.

Ejemplo de uso:

transaction_include_any_action?([:create])



Esto es similar a su primer enfoque, pero solo usa un método (before_save o before_validate para estar realmente seguro) y no veo por qué esto anularía cualquier valor

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

Actualizar

Esta solución no funciona porque, como dice @eleano, ItemObserver es un Singleton, solo tiene una instancia. Entonces, si se guardan 2 elementos al mismo tiempo, @new_record podría tomar su valor del ítem_1 mientras after_commit es activado por item_2. Para superar este problema, debe haber un item.id / mapping para "post-sincornizar" los 2 métodos de devolución de llamada: hackish.


Hoy aprendí que puedes hacer algo como esto:

after_commit :do_something, :on => :create after_commit :do_something, :on => :update

Donde do_ algo es el método de devolución de llamada que desea llamar en ciertas acciones.

Si desea llamar a la misma devolución de llamada para actualizar y crear , pero no destruir , también puede usar: after_commit :do_something, :if => :persisted?

Realmente no está bien documentado y me costó googlearlo. Afortunadamente, conozco a algunas personas brillantes. ¡Espero eso ayude!


Puede cambiar su gancho de evento de after_commit a after_save, para capturar todos los eventos de creación y actualización. Puede usar:

id_changed?

... ayudante en el observador. Esto será cierto en crear y fallar en una actualización.


Puedes resolver usando dos técnicas.

  • El enfoque sugerido por @nathanvda, es decir, verificar el created_at y updated_at. Si son iguales, el registro se acaba de crear, de lo contrario es una actualización.

  • Al usar atributos virtuales en el modelo. Los pasos son:

    • Agregue un campo en el modelo con el código attr_accessor newly_created
    • Actualice lo mismo en las before_update callbacks before_create y before_update callbacks como

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


Tengo curiosidad por saber por qué no puedes mover tu lógica after_create a after_create y after_update . ¿Hay algún cambio de estado importante que ocurra entre las últimas 2 llamadas y after_commit ?

Si su manejo de creación y actualización tiene cierta lógica superpuesta, puede hacer que los dos últimos métodos llamen a un tercer método, pasando la acción:

# 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

Si todavía prefiere usar after_commit, puede usar variables de subprocesos. Esto no perderá memoria siempre y cuando se permita que los subprocesos sean recolectados.

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


Uso el siguiente código para determinar si es un nuevo registro o no:

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

Se basa en la idea de que un nuevo registro tiene un ID predeterminado igual a cero y luego lo cambia al guardar. Por supuesto, el cambio de id no es un caso común, por lo que en la mayoría de los casos la segunda condición se puede omitir.