Comment détecter les changements d'attribut du modèle?

85

Je voudrais créer une fonction de rappel dans les rails qui s'exécute après l'enregistrement d'un modèle.

J'ai ce modèle, Revendication qui a un attribut «statut» qui change en fonction de l'état de la revendication, les valeurs possibles sont en attente, approuvées, approuvées, rejetées

La base de données a «état» avec la valeur par défaut «en attente».

J'aimerais effectuer certaines tâches après la création du modèle la première fois ou la mise à jour d'un état à un autre, en fonction de l'état à partir duquel il change.

Mon idée est d'avoir une fonction dans le modèle:

    after_save :check_state

    def check_state
      # if status changed from nil to pending (created)
      do this

      # if status changed from pending to approved
      performthistask
     end

Ma question est de savoir comment vérifier la valeur précédente avant le changement dans le modèle?

David C
la source

Réponses:

173

Vous devriez regarder le module ActiveModel :: Dirty : Vous devriez pouvoir effectuer les actions suivantes sur votre modèle de réclamation:

claim.status_changed?  # returns true if 'status' attribute has changed
claim.status_was       # returns the previous value of 'status' attribute
claim.status_change    # => ['old value', 'new value'] returns the old and 
                       # new value for 'status' attribute

claim.name = 'Bob'
claim.changed # => ["name"]
claim.changes # => {"name" => ["Bill", "Bob"]}

Oh! les joies des rails!

Harish Shetty
la source
6
Cela ne fonctionnera pas une fois le modèle enregistré, ce qu'il a demandé.
Tom Rossi
4
@TomRossi, les dirtyappels fonctionnent dans after_save(à la fois dans Rails 2.3 et 3.x). Je l'ai utilisé plusieurs fois.
Harish Shetty
11
@TomRossi, les indicateurs modifiés sont réinitialisés après la validation, ils ne seront donc pas disponibles dans les after_commitrappels introduits dans Rails 3.x. Ils y travailleront certainement after_save.
Harish Shetty
Je n'en avais aucune idée! Je pensais qu'ils avaient été réinitialisés une fois qu'ils avaient été enregistrés!
Tom Rossi
5
@TomRossi J'ai commencé avec la même hypothèse il y a quelques années. Quand j'ai essayé de vérifier les drapeaux sales dans after_save, cela a fonctionné. Essentiellement, after_saveest un rappel pour un état entre after DMLet before_commit. Vous pouvez mettre fin à l'ensemble de la transaction en after_savelançant une exception. Si vous voulez faire quelque chose après l'enregistrement sans affecter l'opération en cours, utilisez after_commit:-)
Harish Shetty
38

vous pouvez utiliser ceci

self.changed

il renvoie un tableau de toutes les colonnes qui ont changé dans cet enregistrement

vous pouvez aussi utiliser

self.changes

qui renvoie un hachage des colonnes qui ont changé et des résultats avant et après sous forme de tableaux

zeacuss
la source
7
Juste une petite note pour dire qu'il n'est pas nécessaire d'utiliser self.sur ces derniers - vous pouvez simplement dire changedet changes.
user664833
@ user664833 Plus précisément, vous pouvez omettre selflorsque vous êtes dans le modèle lui-même, mais vous pouvez les appeler sur n'importe quel objet avec object.changedet object.changes. :)
Joshua Pinter
4

Je vous recommande de jeter un œil à l'un des plugins de machine d'état disponibles:

L'un ou l'autre vous permettra de configurer les états et les transitions entre les états. Manière très utile et facile de gérer vos besoins.

Toby Hede
la source
J'essaye le rubyist-aasm. Disons que j'ai la classe Claim <ActiveRecord :: Base include AASM aasm_column: status aasm_initial_state: pending aasm_state: pending,: enter =>: enter_pending def enter_pending Notifier.deliver_pending_notification (self) end end Et mon champ de statut dans ma base de données a la valeur par défaut de «en attente». Si je devais faire un claim.create sans remplir le champ de statut (pour qu'il s'exécute «en attente»), AASM exécutera-t-il la méthode «enter_pending»?
David C
2

Pour Rails 5.1+, vous devez utiliser la méthode d'attribut d'enregistrement actif: saved_change_to_attribute?

save_change_to_attribute? (nom_attr, ** options) `

Cet attribut a-t-il changé lors de notre dernier enregistrement? Cette méthode peut être appelée au saved_change_to_name?lieu de saved_change_to_attribute?("name"). Se comporte de la même manière que attribute_changed?. Cette méthode est utile après les rappels pour déterminer si l'appel de sauvegarde a changé un certain attribut.

Options

from Une fois passée, cette méthode retournera false sauf si la valeur d'origine est égale à l'option donnée

to Une fois passée, cette méthode retournera false à moins que la valeur n'ait été remplacée par la valeur donnée

Donc, votre modèle ressemblera à ceci, si vous voulez appeler une méthode basée sur le changement de valeur d'attribut:

class Claim < ApplicationRecord
  
  after_save :do_this, if: Proc.new { saved_change_to_status?(from: nil, to: 'pending') }

  after_save :do_that, if: Proc.new { saved_change_to_status?(from: 'pending', to: 'approved') }

  
  def do_this
    ..
    ..
  end

  def do_that
    ..
    ..
  end

end

Et si vous ne voulez pas vérifier le changement de valeur dans le rappel, vous pouvez faire ce qui suit:

class Claim < ApplicationRecord

  after_save: :do_this, if: saved_change_to_status?


  def do_this
    ..
    ..
  end

end
Rajkaran Mishra
la source
0

J'ai vu la question se poser dans de nombreux endroits, alors j'ai écrit un minuscule rubygème pour cela, pour rendre le code un peu plus agréable (et éviter un million de déclarations if / else partout): https://github.com/ronna-s / on_change . J'espère que cela aide.

Ronna
la source
0

Vous ferez bien mieux d'utiliser une solution bien testée telle que la gemme state_machine .

Paz Aricha
la source