Determinar quais atributos foram alterados no retorno de chamada Rails after_save?

153

Estou configurando um retorno de chamada after_save no meu observador de modelo para enviar uma notificação apenas se o atributo publicado do modelo tiver sido alterado de falso para verdadeiro. Desde métodos como mudou? são úteis apenas antes de o modelo ser salvo, da maneira que estou tentando (e sem êxito) atualmente é o seguinte:

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

Alguém tem alguma sugestão sobre a melhor maneira de lidar com isso, de preferência usando retornos de chamada de observador de modelo (para não poluir meu código de controlador)?

modulaaron
fonte

Respostas:

182

Rails 5.1+

Use 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 se você preferir saved_change_to_attribute?(:published).

Trilhos 3-5

Aviso

Essa abordagem funciona através do Rails 5.1 (mas está obsoleta no 5.1 e tem alterações recentes no 5.2). Você pode ler sobre a alteração nesta solicitação de recebimento .

No seu after_updatefiltro no modelo, você pode usar o _changed?acessador. Então, por exemplo:

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

Isso simplesmente funciona.

Radek Paviensky
fonte
Esqueça o que eu disse acima - ele não funciona no Rails 2.0.5. Portanto, uma adição útil ao Rails 3. #
stephenr
4
Acho que after_update está obsoleto agora? Enfim, eu tentei isso em um gancho after_save e que parecia funcionar bem. (As mudanças () de hash ainda não foi reajustado ainda em uma after_save, aparentemente.)
Tyler Rick
3
Eu tive que incluir esta linha no arquivo model.rb. incluem ActiveModel :: sujo
coderVishal
13
Nas versões posteriores do Rails, você pode adicionar a condição à after_updatechamada:after_update :send_notification_after_change, if: -> { published_changed? }
Koen.
11
Haverá algumas alterações de API no Rails 5.2. Você precisará fazer saved_change_to_published?ou saved_change_to_publishedbuscar a alteração durante o retorno de chamada
alopez02 04/04
183

Para quem deseja saber as alterações feitas apenas em um after_saveretorno de chamada:

Trilhos 5.1 e superior

model.saved_changes

Trilhos <5,1

model.previous_changes

Consulte também: http://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-previous_changes

Jacek Głodek
fonte
2
Isso funciona perfeitamente quando você não deseja usar retornos de chamada de modelo e precisa que um salvamento válido ocorra antes de executar mais funcionalidades.
Dave Robertson
Isto é perfeito! Obrigado por postar isso.
Jrhicks #
Impressionante! Obrigado por isso.
Daniel Logan
4
Só para deixar claro: nos meus testes (Rails 4), se você estiver usando um after_saveretorno de chamada, self.changed?é truee self.attribute_name_changed?é também true, mas self.previous_changesretorna um hash vazio.
sandre89
9
Isso foi preterido no Rails 5.1 +. saved_changesEm after_savevez disso, use em retornos de chamada
rico_mac
71

Para quem vê isso mais tarde, como atualmente (agosto de 2017) supera o google: vale a pena mencionar, que esse comportamento será alterado no Rails 5.2 e possui avisos de descontinuação no Rails 5.1, pois o ActiveModel :: Dirty mudou um pouco .

O que eu mudo?

Se você estiver usando o attribute_changed?método nos after_*callbacks, verá um aviso como:

AVISO DE DEPRECAÇÃO: O comportamento de attribute_changed?dentro de após retornos de chamada será alterado na próxima versão do Rails. O novo valor de retorno refletirá o comportamento de chamar o método depois de saveretornado (por exemplo, o oposto do que ele retorna agora). Para manter o comportamento atual, use em seu saved_change_to_attribute?lugar. (chamado de some_callback em /PATH_TO/app/models/user.rb:15)

Como ele menciona, você pode corrigir isso facilmente, substituindo a função por saved_change_to_attribute?. Então, por exemplo, name_changed?torna-se saved_change_to_name?.

Da mesma forma, se você estiver usando o attribute_changepara obter os valores antes e depois, isso também muda e gera o seguinte:

AVISO DE DEPRECAÇÃO: O comportamento de attribute_changedentro de após retornos de chamada será alterado na próxima versão do Rails. O novo valor de retorno refletirá o comportamento de chamar o método depois de saveretornado (por exemplo, o oposto do que ele retorna agora). Para manter o comportamento atual, use em seu saved_change_to_attributelugar. (chamado de some_callback em /PATH_TO/app/models/user.rb:20)

Novamente, como mencionado, o método altera o nome para o saved_change_to_attributequal retorna ["old", "new"]. ou use saved_changes, que retorna todas as alterações, e essas podem ser acessadas como saved_changes['attribute'].

Frederik Spang
fonte
2
Observe que esta resposta útil também inclui soluções alternativas para a reprovação de attribute_wasmétodos: use em saved_change_to_attributevez disso.
21818 Joe Atzberger
47

Caso você possa fazer isso em before_savevez de after_save, poderá usar isso:

self.changed

retorna uma matriz de todas as colunas alteradas nesse registro.

você também pode usar:

self.changes

que retorna um hash de colunas que foram alteradas e os resultados anteriores e posteriores como matrizes

zeacuss
fonte
8
Exceto que eles não funcionam em um after_retorno de chamada, que é sobre o que a questão realmente era. A resposta de @ jacek-głodek abaixo é a correta.
Jazz
Atualizado a resposta para deixar claro que isso só se aplica abefore_save
mahemoff 04/04
1
Qual versão do Rails é essa? No Rails 4, self.changedpode ser usado em after_saveretornos de chamada.
sandre89
Funciona bem! Também a ser observado, o resultado de self.changedé uma matriz de strings! (Não é um símbolo!) #["attr_name", "other_attr_name"]
1874
8

A resposta "selecionada" não funcionou para mim. Estou usando o rails 3.1 com CouchRest :: Model (baseado no modelo ativo). Os _changed?métodos não retornam true para atributos alterados no after_updategancho, apenas no before_updategancho. Consegui fazê-lo funcionar usando o around_updategancho (novo?) :

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
fonte
1
A resposta selecionada também não funcionou para mim, mas funcionou. Obrigado! Estou usando o ActiveRecord 3.2.16.
Ben Lee
5

você pode adicionar uma condição after_updatesemelhante à seguinte:

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

  ...
end

não há necessidade de adicionar uma condição no send_notificationpróprio método.

eco
fonte
-17

Você acabou de adicionar um acessador que define o que você altera

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
fonte
Essa é uma prática muito ruim, nem considere.
Nuno Silva