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])
Eche un vistazo al código de prueba: https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb
Ahí puedes encontrar:
after_commit(:on => :create)
after_commit(:on => :update)
after_commit(:on => :destroy)
y
after_rollback(:on => :create)
after_rollback(:on => :update)
after_rollback(:on => :destroy)
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
ybefore_update callbacks
comodef before_create (record) record.newly_created = true end def before_update (record) record.newly_created = false end
- Agregue un campo en el modelo con el código
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.