Déterminer quels attributs ont été modifiés dans le rappel de Rails after_save?

153

Je configure un rappel after_save dans mon observateur de modèle pour envoyer une notification uniquement si l' attribut publié du modèle est passé de faux à vrai. Depuis des méthodes telles que changé? ne sont utiles qu'avant l'enregistrement du modèle, la façon dont j'essaie actuellement (et sans succès) de le faire est la suivante:

def before_save(blog)
  @og_published = blog.published?
end

def after_save(blog)
  if @og_published == false and blog.published? == true
    Notification.send(...)
  end
end

Quelqu'un a-t-il des suggestions sur la meilleure façon de gérer cela, de préférence en utilisant les rappels d'observateur de modèle (afin de ne pas polluer le code de mon contrôleur)?

modulaaron
la source

Réponses:

182

Rails 5.1+

Utilisez saved_change_to_published?:

class SomeModel < ActiveRecord::Base
  after_update :send_notification_after_change

  def send_notification_after_change
    Notification.send(…) if (saved_change_to_published? && self.published == true)
  end

end

Ou si vous préférez, saved_change_to_attribute?(:published).

Rails 3–5,1

avertissement

Cette approche fonctionne avec Rails 5.1 (mais est obsolète dans la version 5.1 et comporte des changements de rupture dans la version 5.2). Vous pouvez en savoir plus sur le changement dans cette pull request .

Dans votre after_updatefiltre sur le modèle, vous pouvez utiliser l' _changed?accesseur. Donc par exemple:

class SomeModel < ActiveRecord::Base
  after_update :send_notification_after_change

  def send_notification_after_change
    Notification.send(...) if (self.published_changed? && self.published == true)
  end

end

Cela fonctionne juste.

Radek Paviensky
la source
Oubliez ce que j'ai dit ci-dessus - cela ne fonctionne PAS dans Rails 2.0.5. Donc un ajout utile à Rails 3.
stephenr
4
Je pense que after_update est obsolète maintenant? Quoi qu'il en soit, j'ai essayé ceci dans un hook after_save et cela semblait fonctionner correctement. (Le hachage des modifications () n'a toujours pas été réinitialisé dans un after_save, apparemment.)
Tyler Rick
3
J'ai dû inclure cette ligne dans le fichier model.rb. include ActiveModel :: Dirty
coderVishal le
13
Dans les versions ultérieures de Rails, vous pouvez ajouter la condition à l' after_updateappel:after_update :send_notification_after_change, if: -> { published_changed? }
Koen.
11
Il y aura quelques changements d'API dans Rails 5.2. Vous devrez faire saved_change_to_published?ou saved_change_to_publishedrécupérer la modification pendant le rappel
alopez02
183

Pour ceux qui veulent connaître les modifications qui viennent d'être apportées lors d'un after_saverappel:

Rails 5.1 et plus

model.saved_changes

Rails <5,1

model.previous_changes

Voir également: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes

Jacek Głodek
la source
2
Cela fonctionne parfaitement lorsque vous ne souhaitez pas utiliser les rappels de modèle et que vous avez besoin d'une sauvegarde valide avant d'exécuter d'autres fonctionnalités.
Dave Robertson
C'est parfait! Merci d'avoir posté ceci.
jrhicks
Impressionnant! Merci pour cela.
Daniel Logan
4
Juste pour être clair: dans mes tests (Rails 4), si vous utilisez un after_savecallback, self.changed?est trueet self.attribute_name_changed?est aussi true, mais self.previous_changesrenvoie un hachage vide.
sandre89
9
Ceci est obsolète dans Rails 5.1 +. Utilisez plutôt saved_changesdans les after_saverappels
rico_mac
71

À tous ceux qui verront cela plus tard, comme il est actuellement (août 2017) en tête de Google: Il convient de mentionner que ce comportement sera modifié dans Rails 5.2 et qu'il a des avertissements de dépréciation à partir de Rails 5.1, car ActiveModel :: Dirty a un peu changé .

Qu'est-ce que je change?

Si vous utilisez la attribute_changed?méthode dans les after_*-callbacks, vous verrez un avertissement comme:

AVERTISSEMENT DE DEPRECATION: Le comportement des attribute_changed?callbacks inside ou after changera dans la prochaine version de Rails. La nouvelle valeur de retour reflétera le comportement de l'appel de la méthode après son saveretour (par exemple, le contraire de ce qu'elle renvoie maintenant). Pour conserver le comportement actuel, utilisez saved_change_to_attribute?plutôt. (appelé depuis some_callback à /PATH_TO/app/models/user.rb:15)

Comme il le mentionne, vous pouvez résoudre ce problème facilement en remplaçant la fonction par saved_change_to_attribute?. Ainsi, par exemple, name_changed?devient saved_change_to_name?.

De même, si vous utilisez attribute_changepour obtenir les valeurs avant-après, cela change également et lance ce qui suit:

AVERTISSEMENT DE DEPRECATION: Le comportement des attribute_changecallbacks inside ou after changera dans la prochaine version de Rails. La nouvelle valeur de retour reflétera le comportement de l'appel de la méthode après son saveretour (par exemple, le contraire de ce qu'elle renvoie maintenant). Pour conserver le comportement actuel, utilisez saved_change_to_attributeplutôt. (appelé depuis some_callback à /PATH_TO/app/models/user.rb:20)

Encore une fois, comme il le mentionne, la méthode change le nom vers saved_change_to_attributelequel retourne ["old", "new"]. ou use saved_changes, qui renvoie toutes les modifications, et celles-ci sont accessibles en tant que saved_changes['attribute'].

Frederik Spang
la source
2
Notez que cette réponse utile inclut également des solutions de contournement pour la dépréciation des attribute_wasméthodes: utilisez saved_change_to_attributeplutôt.
Joe Atzberger
47

Si vous pouvez le faire à la before_saveplace de after_save, vous pourrez utiliser ceci:

self.changed

il renvoie un tableau de toutes les colonnes modifiées de cet enregistrement.

vous pouvez aussi utiliser:

self.changes

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

zeacuss
la source
8
Sauf que ceux-ci ne fonctionnent pas dans un after_rappel, ce qui était en fait l'objet de la question. La réponse de @ jacek-głodek ci-dessous est la bonne.
Jazz
Mise à jour de la réponse pour préciser que cela ne s'applique qu'àbefore_save
mahemoff
1
De quelle version de Rails s'agit-il? Dans Rails 4, self.changedpeut être utilisé dans les after_saverappels.
sandre89
Fonctionne très bien! À noter également, le résultat de self.changedest un tableau de chaînes! (Pas de symboles!)["attr_name", "other_attr_name"]
LukeS
8

La réponse «sélectionnée» n'a pas fonctionné pour moi. J'utilise les rails 3.1 avec CouchRest :: Model (basé sur Active Model). Les _changed?méthodes ne retournent pas true pour les attributs modifiés dans le after_updatehook, uniquement dans le before_updatehook. J'ai pu le faire fonctionner en utilisant le (nouveau?) around_updateHook:

class SomeModel < ActiveRecord::Base
  around_update :send_notification_after_change

  def send_notification_after_change
    should_send_it = self.published_changed? && self.published == true

    yield

    Notification.send(...) if should_send_it
  end

end
Jeff Gran
la source
1
La réponse choisie n'a pas non plus fonctionné pour moi, mais cela a fonctionné. Merci! J'utilise ActiveRecord 3.2.16.
Ben Lee
5

vous pouvez ajouter une condition after_updatesimilaire pour:

class SomeModel < ActiveRecord::Base
  after_update :send_notification, if: :published_changed?

  ...
end

il n'est pas nécessaire d'ajouter une condition dans la send_notificationméthode elle-même.

écho
la source
-17

Vous ajoutez simplement un accesseur qui définit ce que vous modifiez

class Post < AR::Base
  attr_reader :what_changed

  before_filter :what_changed?

  def what_changed?
    @what_changed = changes || []
  end

  after_filter :action_on_changes

  def action_on_changes
    @what_changed.each do |change|
      p change
    end
  end
end
Shingara
la source
C'est une très mauvaise pratique, n'y pensez même pas.
Nuno Silva