J'ai un observateur et j'enregistre un rappel after_commit
. Comment savoir s'il a été déclenché après la création ou la mise à jour? Je peux dire qu'un élément a été détruit en demandant item.destroyed?
Mais #new_record?
Ne fonctionne pas depuis que l'élément a été enregistré.
J'allais le résoudre en ajoutant after_create
/after_update
Et faire quelque chose comme @action = :create
À l'intérieur et vérifier le @action
À after_commit
, Mais il semble que l'instance d'observateur est un singleton et je pourrais simplement remplacer une valeur avant qu'elle n'atteigne le after_commit
. Je l'ai donc résolu de manière plus laide, en stockant l'action dans une carte basée sur item.id sur after_create/update et en vérifiant sa valeur sur after_commit. Vraiment moche.
Est-ce qu'il y a un autre moyen?
Comme l'a dit @tardate, transaction_include_action?
Est une bonne indication, bien que ce soit une méthode privée, et dans un observateur, il devrait être accessible avec #send
.
class ProductScoreObserver < ActiveRecord::Observer
observe :product
def after_commit(product)
if product.send(:transaction_include_action?, :destroy)
...
Malheureusement, l'option :on
Ne fonctionne pas dans les observateurs.
Assurez-vous simplement de tester l'enfer de vos observateurs (recherchez la gemme test_after_commit
Si vous utilisez use_transactional_fixtures) donc quand vous passez à la nouvelle version Rails, vous saurez si cela fonctionne toujours .
(Testé le 3.2.9)
Au lieu d'observateurs, j'utilise maintenant ActiveSupport :: Concern et after_commit :blah, on: :create
Y fonctionne.
Je pense que transaction_include_action? est ce que vous recherchez. Il donne une indication fiable de la transaction spécifique en cours (vérifiée en 3.0.8).
Formellement, il détermine si une transaction comprend une action pour: créer,: mettre à jour ou: détruire. Utilisé pour filtrer les rappels.
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
Il peut également être intéressant transaction_record_state qui peut être utilisé pour déterminer si un enregistrement a été créé ou détruit dans une transaction. L'état doit être: new_record ou: destroy.
Mise à jour pour Rails 4
Pour ceux qui cherchent à résoudre le problème dans Rails 4, cette méthode est maintenant déconseillée, vous devez utiliser transaction_include_any_action?
qui accepte un array
d'actions.
Exemple d'utilisation:
transaction_include_any_action?([:create])
J'ai appris aujourd'hui que vous pouvez faire quelque chose comme ça:
after_commit :do_something, :on => :create
after_commit :do_something, :on => :update
Où do_something est la méthode de rappel que vous souhaitez appeler pour certaines actions.
Si vous souhaitez appeler le même rappel pour pdate et create, mais pas destroy, vous pouvez également utiliser: after_commit :do_something, :if => :persisted?
Ce n'est vraiment pas bien documenté et j'ai eu du mal à le googler. Heureusement, je connais quelques personnes brillantes. J'espère que cela aide!
Vous pouvez résoudre en utilisant deux techniques.
L'approche suggérée par @nathanvda, c'est-à-dire la vérification de created_at et updated_at. S'ils sont identiques, l'enregistrement est nouvellement créé, sinon c'est une mise à jour.
En utilisant des attributs virtuels dans le modèle. Les étapes sont les suivantes:
attr_accessor newly_created
Mettre à jour la même chose dans le before_create
et before_update callbacks
as
def before_create (record)
record.newly_created = true
end
def before_update (record)
record.newly_created = false
end
Basé sur l'idée de leenasn, j'ai créé quelques modules qui permettent d'utiliser after_commit_on_update
et after_commit_on_create
rappels: 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
Jetez un œil au code de test: https://github.com/Rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb
Vous y trouverez:
after_commit(:on => :create)
after_commit(:on => :update)
after_commit(:on => :destroy)
et
after_rollback(:on => :create)
after_rollback(:on => :update)
after_rollback(:on => :destroy)
Je suis curieux de savoir pourquoi vous ne pouviez pas déplacer votre after_commit
logique dans after_create
et after_update
. Y a-t-il un changement d'état important entre les deux derniers appels et after_commit
?
Si votre gestion de création et de mise à jour a une logique qui se chevauche, vous pouvez simplement demander aux deux dernières méthodes d'appeler une troisième méthode, en transmettant l'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
Si vous utilisez toujours plutôt after_commit, vous pouvez utiliser des variables de thread. Cela ne fuira pas la mémoire tant que les threads morts peuvent être récupérés.
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
J'utilise le code suivant pour déterminer s'il s'agit d'un nouvel enregistrement ou non:
previous_changes[:id] && previous_changes[:id][0].nil?
Il est basé sur l'idée qu'un nouvel enregistrement a un identifiant par défaut égal à zéro, puis le change lors de la sauvegarde. Bien sûr, le changement d'ID n'est pas un cas courant, donc dans la plupart des cas, la deuxième condition peut être omise.