web-dev-qa-db-fra.com

Rails 3: Comment identifier l'action after_commit chez les observateurs? (Créer / mettre à jour / détruire)

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?

Mise à jour

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)

Mise à jour 2

Au lieu d'observateurs, j'utilise maintenant ActiveSupport :: Concern et after_commit :blah, on: :create Y fonctionne.

60
elado

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])
55
tardate

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

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!

56
Jure Triglav

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:

    • Ajoutez un champ dans le modèle avec le code 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
      
7
leenasn

Basé sur l'idée de leenasn, j'ai créé quelques modules qui permettent d'utiliser after_commit_on_updateet 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
3
lacco

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)
2
kenn

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
0
Kelvin

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.

0
hwo411