edge
Mais em rubyonrails.org: Mais Ruby on Rails

Callbacks do Active Record

Este guia ensina como se conectar ao ciclo de vida dos objetos Active Record.

Depois de ler este guia, você saberá:

1 O Ciclo de Vida do Objeto

Durante a operação normal de uma aplicação Rails, objetos podem ser criados, atualizados e destruídos. O Active Record fornece ganchos para este ciclo de vida do objeto para que você possa controlar sua aplicação e seus dados.

Callbacks permitem que você acione lógica antes ou depois de uma alteração no estado de um objeto.

class Baby < ApplicationRecord
  after_create -> { puts "Parabéns!" }
end
irb> @baby = Baby.create
Parabéns!

Como você verá, existem muitos eventos do ciclo de vida e você pode escolher se conectar a qualquer um deles antes, depois ou até mesmo ao redor deles.

2 Visão geral dos Callbacks

Callbacks são métodos que são chamados em determinados momentos do ciclo de vida de um objeto. Com callbacks, é possível escrever código que será executado sempre que um objeto Active Record for criado, salvo, atualizado, excluído, validado ou carregado do banco de dados.

2.1 Registro de Callbacks

Para usar os callbacks disponíveis, você precisa registrá-los. Você pode implementar os callbacks como métodos comuns e usar um método de classe no estilo macro para registrá-los como callbacks:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_validation :ensure_login_has_a_value

  private
    def ensure_login_has_a_value
      if login.blank?
        self.login = email unless email.blank?
      end
    end
end

Os métodos de classe no estilo macro também podem receber um bloco. Considere usar este estilo se o código dentro do seu bloco for tão curto que caiba em uma única linha:

class User < ApplicationRecord
  validates :login, :email, presence: true

  before_create do
    self.name = login.capitalize if name.blank?
  end
end

Alternativamente, você pode passar um proc para o callback a ser acionado.

class User < ApplicationRecord
  before_create ->(user) { user.name = user.login.capitalize if user.name.blank? }
end

Por fim, você pode definir seu próprio objeto de callback personalizado, que abordaremos mais detalhadamente abaixo.

class User < ApplicationRecord
  before_create MaybeAddName
end

class MaybeAddName
  def self.before_create(record)
    if record.name.blank?
      record.name = record.login.capitalize
    end
  end
end

Callbacks também podem ser registrados para serem acionados apenas em determinados eventos do ciclo de vida, o que permite controle completo sobre quando e em qual contexto seus callbacks são acionados.

class User < ApplicationRecord
  before_validation :normalize_name, on: :create

  # :on também aceita um array
  after_validation :set_location, on: [ :create, :update ]

  private
    def normalize_name
      self.name = name.downcase.titleize
    end

    def set_location
      self.location = LocationService.query(self)
    end
end

É considerada uma boa prática declarar os métodos de callback como privados. Se deixados públicos, eles podem ser chamados de fora do modelo e violar o princípio de encapsulamento do objeto.

CUIDADO. Evite chamadas para update, save ou outros métodos que criam efeitos colaterais no objeto dentro do seu callback. Por exemplo, não chame update(attribute: "value") dentro de um callback. Isso pode alterar o estado do modelo e pode resultar em efeitos colaterais inesperados durante o commit. Em vez disso, você pode atribuir valores diretamente com segurança (por exemplo, self.attribute = "value") em before_create / before_update ou callbacks anteriores.

3 Callbacks Disponíveis

Aqui está uma lista com todos os callbacks disponíveis do Active Record, listados na mesma ordem em que serão chamados durante as respectivas operações:

3.1 Criando um Objeto

3.2 Atualizando um Objeto

CUIDADO. after_save é executado tanto na criação quanto na atualização, mas sempre depois dos callbacks mais específicos after_create e after_update, independentemente da ordem em que as chamadas de macro foram executadas.

3.3 Destruindo um Objeto

NOTA: Os callbacks before_destroy devem ser colocados antes das associações dependent: :destroy (ou use a opção prepend: true), para garantir que eles sejam executados antes que os registros sejam excluídos por dependent: :destroy.

CUIDADO. after_commit oferece garantias muito diferentes de after_save, after_update e after_destroy. Por exemplo, se ocorrer uma exceção em um after_save, a transação será revertida e os dados não serão persistidos. Enquanto qualquer coisa que aconteça after_commit pode garantir que a transação já foi concluída e os dados foram persistidos no banco de dados. Mais sobre callbacks transacionais abaixo.

3.4 after_initialize e after_find

Sempre que um objeto Active Record for instanciado, o callback after_initialize será chamado, seja diretamente usando new ou quando um registro for carregado do banco de dados. Isso pode ser útil para evitar a necessidade de substituir diretamente o método initialize do Active Record.

Ao carregar um registro do banco de dados, o callback after_find será chamado. after_find é chamado antes de after_initialize se ambos forem definidos.

NOTA: Os callbacks after_initialize e after_find não têm contrapartes before_*.

Eles podem ser registrados da mesma forma que os outros callbacks do Active Record.

class User < ApplicationRecord
  after_initialize do |user|
    puts "Você inicializou um objeto!"
  end

  after_find do |user|
    puts "Você encontrou um objeto!"
  end
end
irb> User.new
Você inicializou um objeto!
=> #<User id: nil>

irb> User.first
Você encontrou um objeto!
Você inicializou um objeto!
=> #<User id: 1>

3.5 after_touch

O callback after_touch será chamado sempre que um objeto Active Record for tocado.

class User < ApplicationRecord
  after_touch do |user|
    puts "Você tocou em um objeto"
  end
end
irb> u = User.create(name: 'Kuldeep')
=> #<User id: 1, name: "Kuldeep", created_at: "2013-11-25 12:17:49", updated_at: "2013-11-25 12:17:49">

irb> u.touch
Você tocou em um objeto
=> true

Ele pode ser usado junto com belongs_to:

class Book < ApplicationRecord
  belongs_to :library, touch: true
  after_touch do
    puts 'Um livro foi tocado'
  end
end

class Library < ApplicationRecord
  has_many :books
  after_touch :log_when_books_or_library_touched

  private
    def log_when_books_or_library_touched
      puts 'Livro/Biblioteca foi tocado'
    end
end
irb> @book = Book.last
=> #<Book id: 1, library_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">

irb> @book.touch # aciona @book.library.touch
Um livro foi tocado
Livro/Biblioteca foi tocado
=> true

4 Executando Callbacks

Os seguintes métodos acionam callbacks:

  • create
  • create!
  • destroy
  • destroy!
  • destroy_all
  • destroy_by
  • save
  • save!
  • save(validate: false)
  • toggle!
  • touch
  • update_attribute
  • update
  • update!
  • valid?

Além disso, o callback after_find é acionado pelos seguintes métodos de busca:

  • all
  • first
  • find
  • find_by
  • find_by_*
  • find_by_*!
  • find_by_sql
  • last

O callback after_initialize é acionado toda vez que um novo objeto da classe é inicializado.

NOTA: Os métodos find_by_* e find_by_*! são finders dinâmicos gerados automaticamente para cada atributo. Saiba mais sobre eles na seção Finders dinâmicos

5 Ignorando Callbacks

Assim como nas validações, também é possível ignorar callbacks usando os seguintes métodos:

  • decrement!
  • decrement_counter
  • delete
  • delete_all
  • delete_by
  • increment!
  • increment_counter
  • insert
  • insert!
  • insert_all
  • insert_all!
  • touch_all
  • update_column
  • update_columns
  • update_all
  • update_counters
  • upsert
  • upsert_all

Esses métodos devem ser usados com cautela, no entanto, porque regras de negócio importantes e lógica de aplicativo podem ser mantidas em callbacks. Ignorá-los sem entender as possíveis implicações pode levar a dados inválidos.

6 Interrompendo a Execução

Ao registrar novos callbacks para seus modelos, eles serão enfileirados para execução. Essa fila incluirá todas as validações do seu modelo, os callbacks registrados e a operação de banco de dados a ser executada.

Toda a cadeia de callbacks é envolvida em uma transação. Se algum callback gerar uma exceção, a cadeia de execução é interrompida e um ROLLBACK é emitido. Para interromper intencionalmente uma cadeia, use:

throw :abort

ATENÇÃO. Qualquer exceção que não seja ActiveRecord::Rollback ou ActiveRecord::RecordInvalid será reemitida pelo Rails após a interrupção da cadeia de callbacks. Além disso, pode quebrar o código que não espera que métodos como save e update (que normalmente tentam retornar true ou false) gerem uma exceção.

NOTA: Se um ActiveRecord::RecordNotDestroyed for gerado dentro do callback after_destroy, before_destroy ou around_destroy, ele não será reemitido e o método destroy retornará false.

7 Callbacks Relacionais

Callbacks funcionam por meio de relacionamentos de modelos e até podem ser definidos por eles. Suponha um exemplo em que um usuário tem muitos artigos. Os artigos de um usuário devem ser destruídos se o usuário for destruído. Vamos adicionar um callback after_destroy ao modelo User por meio de seu relacionamento com o modelo Article:

class User < ApplicationRecord
  has_many :articles, dependent: :destroy
end

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    puts 'Artigo destruído'
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Artigo destruído
=> #<User id: 1>

8 Callbacks Condicionais

Assim como nas validações, também podemos condicionar a chamada de um método de callback com base em um predicado específico. Podemos fazer isso usando as opções :if e :unless, que podem receber um símbolo, um Proc ou um Array.

Você pode usar a opção :if quando desejar especificar em quais condições o callback deve ser chamado. Se você quiser especificar as condições em que o callback não deve ser chamado, pode usar a opção :unless.

8.1 Usando :if e :unless com um Symbol

Você pode associar as opções :if e :unless a um símbolo correspondente ao nome de um método predicado que será chamado imediatamente antes do callback.

Ao usar a opção :if, o callback não será executado se o método predicado retornar false; ao usar a opção :unless, o callback não será executado se o método predicado retornar true. Essa é a opção mais comum.

class Order < ApplicationRecord
  before_save :normalize_card_number, if: :paid_with_card?
end

Usando essa forma de registro, também é possível registrar vários predicados diferentes que devem ser chamados para verificar se o callback deve ser executado. Abordaremos isso abaixo.

8.2 Usando :if e :unless com um Proc

É possível associar :if e :unless a um objeto Proc. Essa opção é mais adequada ao escrever métodos de validação curtos, geralmente em uma única linha:

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: Proc.new { |order| order.paid_with_card? }
end

Como o proc é avaliado no contexto do objeto, também é possível escrever assim:

class Order < ApplicationRecord
  before_save :normalize_card_number, if: Proc.new { paid_with_card? }
end

8.3 Múltiplas Condições de Callback

As opções :if e :unless também aceitam um array de procs ou nomes de métodos como símbolos:

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, :untrusted_author?]
end

Você pode incluir facilmente um proc na lista de condições:

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, Proc.new { untrusted_author? }]
end

8.4 Usando Ambos :if e :unless

Callbacks podem misturar tanto :if quanto :unless na mesma declaração:

class Comment < ApplicationRecord
  before_save :filter_content,
    if: Proc.new { forum.parental_control? },
    unless: Proc.new { author.trusted? }
end

O callback só é executado quando todas as condições :if são avaliadas como true e nenhuma das condições :unless são avaliadas como true.

9 Classes de Callback

Às vezes, os métodos de callback que você escrever serão úteis o suficiente para serem reutilizados por outros modelos. O Active Record permite criar classes que encapsulam os métodos de callback, para que possam ser reutilizados.

Aqui está um exemplo em que criamos uma classe com um callback after_destroy para lidar com a limpeza de arquivos descartados no sistema de arquivos. Esse comportamento pode não ser exclusivo do nosso modelo PictureFile e podemos querer compartilhá-lo, então é uma boa ideia encapsulá-lo em uma classe separada. Isso tornará mais fácil testar e alterar esse comportamento.

class FileDestroyerCallback
  def after_destroy(file)
    if File.exist?(file.filepath)
      File.delete(file.filepath)
    end
  end
end

Quando declarados dentro de uma classe, como acima, os métodos de callback receberão o objeto do modelo como parâmetro. Isso funcionará em qualquer modelo que use a classe da seguinte maneira:

class PictureFile < ApplicationRecord
  after_destroy FileDestroyerCallback.new
end

Observe que precisamos instanciar um novo objeto FileDestroyerCallback, pois declaramos nosso callback como um método de instância. Isso é particularmente útil se os callbacks usarem o estado do objeto instanciado. No entanto, muitas vezes fará mais sentido declarar os callbacks como métodos de classe:

class FileDestroyerCallback
  def self.after_destroy(file)
    if File.exist?(file.filepath)
      File.delete(file.filepath)
    end
  end
end

Quando o método de callback é declarado dessa maneira, não será necessário instanciar um novo objeto FileDestroyerCallback em nosso modelo.

class PictureFile < ApplicationRecord
  after_destroy FileDestroyerCallback
end

Você pode declarar quantos callbacks desejar dentro de suas classes de callback.

10 Callbacks de Transação

10.1 Lidando com Consistência

Existem dois callbacks adicionais que são acionados após a conclusão de uma transação de banco de dados: after_commit e after_rollback. Esses callbacks são muito semelhantes ao callback after_save, exceto que eles só são executados após as alterações no banco de dados terem sido confirmadas ou revertidas. Eles são mais úteis quando seus modelos Active Record precisam interagir com sistemas externos que não fazem parte da transação do banco de dados. Considere, por exemplo, o exemplo anterior em que o modelo PictureFile precisa excluir um arquivo após o registro correspondente ser destruído. Se algo levantar uma exceção após a chamada de retorno after_destroy e a transação for revertida, o arquivo terá sido excluído e o modelo ficará em um estado inconsistente. Por exemplo, suponha que picture_file_2 no código abaixo não seja válido e o método save! levante um erro.

PictureFile.transaction do
  picture_file_1.destroy
  picture_file_2.save!
end

Usando a chamada de retorno after_commit, podemos lidar com esse caso.

class PictureFile < ApplicationRecord
  after_commit :delete_picture_file_from_disk, on: :destroy

  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

NOTA: A opção :on especifica quando uma chamada de retorno será disparada. Se você não fornecer a opção :on, a chamada de retorno será disparada para todas as ações.

10.2 O Contexto Importa

Como é comum usar a chamada de retorno after_commit apenas em criação, atualização ou exclusão, existem aliases para essas operações:

class PictureFile < ApplicationRecord
  after_destroy_commit :delete_picture_file_from_disk

  def delete_picture_file_from_disk
    if File.exist?(filepath)
      File.delete(filepath)
    end
  end
end

AVISO. Quando uma transação é concluída, as chamadas de retorno after_commit ou after_rollback são chamadas para todos os modelos criados, atualizados ou excluídos dentro dessa transação. No entanto, se uma exceção for levantada em uma dessas chamadas de retorno, a exceção será propagada e quaisquer métodos after_commit ou after_rollback restantes não serão executados. Portanto, se o código da chamada de retorno puder levantar uma exceção, você precisará resgatá-la e tratá-la dentro da chamada de retorno para permitir que outras chamadas de retorno sejam executadas.

AVISO. O código executado dentro das chamadas de retorno after_commit ou after_rollback não está contido em uma transação.

AVISO. Usar tanto after_create_commit quanto after_update_commit com o mesmo nome de método permitirá apenas que a última chamada de retorno definida tenha efeito, pois ambas são aliases internamente para after_commit, que substitui as chamadas de retorno previamente definidas com o mesmo nome de método.

class User < ApplicationRecord
  after_create_commit :log_user_saved_to_db
  after_update_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      puts 'Usuário foi salvo no banco de dados'
    end
end
irb> @user = User.create # não imprime nada

irb> @user.save # atualizando @user
Usuário foi salvo no banco de dados

10.3 after_save_commit

Também existe o after_save_commit, que é um alias para usar a chamada de retorno after_commit tanto para criação quanto para atualização juntas:

class User < ApplicationRecord
  after_save_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      puts 'Usuário foi salvo no banco de dados'
    end
end
irb> @user = User.create # criando um Usuário
Usuário foi salvo no banco de dados

irb> @user.save # atualizando @user
Usuário foi salvo no banco de dados

10.4 Ordem das Chamadas de Retorno Transacionais

Ao definir várias chamadas de retorno transacionais (after_commit, after_rollback, etc), a ordem será invertida em relação à ordem em que foram definidas.

class User < ActiveRecord::Base
  after_commit { puts("isso é realmente chamado em segundo lugar") }
  after_commit { puts("isso é realmente chamado em primeiro lugar") }
end

NOTA: Isso se aplica a todas as variações de after_*_commit, como after_destroy_commit.

Feedback

Você é incentivado a ajudar a melhorar a qualidade deste guia.

Por favor, contribua se encontrar algum erro de digitação ou factual. Para começar, você pode ler nossa contribuição à documentação seção.

Você também pode encontrar conteúdo incompleto ou desatualizado. Por favor, adicione qualquer documentação ausente para o principal. Certifique-se de verificar Guias Edge primeiro para verificar se os problemas já foram corrigidos ou não no branch principal. Verifique as Diretrizes dos Guias do Ruby on Rails para estilo e convenções.

Se por algum motivo você encontrar algo para corrigir, mas não puder corrigi-lo você mesmo, por favor abra uma issue.

E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem-vinda no Fórum oficial do Ruby on Rails.