Rails 3: Comment identifier l’action after_commit dans les observateurs? (créer / mettre à jour / détruire)

J’ai un observateur et after_commit un rappel after_commit . Comment puis-je savoir s’il a été déclenché après la création ou la mise à jour? Je peux dire qu’un object a été détruit en demandant item.destroyed? mais #new_record? ne fonctionne pas depuis que l’article a été enregistré.

J’allais le résoudre en ajoutant after_create / after_update et faire quelque chose comme @action = :create inside et vérifier la @action à after_commit , mais il semble que l’instance de l’observateur soit un singleton et que je puisse simplement remplacer une valeur avant qu’elle arrive à le after_commit . Donc, je l’ai résolu de manière plus moche, en stockant l’action dans une carte basée sur le 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?

Mettre à jour

Comme @tardate a dit, transaction_include_action? est une bonne indication, même si c’est une méthode privée, et dans un observateur, il faut y accéder 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 de tester vos observateurs (recherchez gem test_after_commit si vous utilisez use_transactional_fixtures), donc lorsque vous mettrez à niveau vers la nouvelle version de Rails, vous saurez si cela fonctionne toujours.

(Testé sur 3.2.9)

Mise à jour 2

Au lieu d’observateurs, j’utilise maintenant ActiveSupport :: Concern et after_commit :blah, on: :create works there.

Je pense transaction_include_action? est ce que vous êtes après. 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 inclut une action pour: create,: update ou: destroy. Utilisé dans le filtrage des 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 se peut également que transaction_record_state puisse être utilisé pour déterminer si un enregistrement a été créé ou détruit dans une transaction. L'état doit être l'un de: new_record ou: détruit.

Mise à jour pour Rails 4

Pour ceux qui cherchent à résoudre le problème dans Rails 4, cette méthode est maintenant obsolète, vous devriez 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 ceci:

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

do_something est la méthode de rappel que vous souhaitez appeler sur certaines actions.

Si vous voulez appeler le même rappel pour la mise à jour et créer , mais pas détruire , 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 shinyes. J’espère que cela aide!

Vous pouvez résoudre en utilisant deux techniques.

  • L’approche suggérée par @nathanvda, c’est-à-dire vérifier les created_at et updated_at. S’ils sont identiques, l’enregistrement est nouvellement créé, sinon c’est une mise à jour.

  • En utilisant des atsortingbuts virtuels dans le modèle. Les étapes sont les suivantes:

    • Ajouter un champ dans le modèle avec le code attr_accessor newly_created
    • Mettez à jour le même dans les before_create et before_update callbacks que

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

Sur la base de l’idée de leenasn, j’ai créé des modules permettant d’utiliser les after_commit_on_update et 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 

Jetez un oeil au code de test: https://github.com/rails/rails/blob/master/activerecord/test/cases/transaction_callbacks_test.rb

Là vous pouvez trouver:

 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 logique after_create 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 appeler les trois dernières méthodes, en passant 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 préférez toujours utiliser after_commit, vous pouvez utiliser des variables de thread. Cela ne provoquera pas de fuite de mémoire tant que les threads morts pourront ê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 

Ceci est similaire à votre 1ère approche mais il n’utilise qu’une seule méthode (before_save ou before_validate pour être vraiment sûr) et je ne vois pas pourquoi cela remplacerait une valeur

 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 

Mettre à jour

Cette solution ne fonctionne pas car, comme indiqué par @eleano, ItemObserver est un singleton, il ne possède qu’une seule instance. Donc, si 2 Item est enregistré en même temps, @new_record pourrait prendre sa valeur de item_1 alors que after_commit est déclenché par item_2. Pour surmonter ce problème, il devrait y avoir un item.id vérifiant / mappant pour “post-synchroniser” les 2 méthodes de rappel: hackish.

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 l’enregistrement. Bien sûr, la modification des identifiants n’est pas un cas commun, de sorte que dans la plupart des cas, la deuxième condition peut être omise.

Vous pouvez remplacer votre hook d’événement after_commit par after_save pour capturer tous les événements de création et de mise à jour. Vous pouvez alors utiliser:

 id_changed? 

… aide à l’observateur. Ce sera vrai sur create et false sur une mise à jour.