NO LEAS ESTE ARCHIVO EN GITHUB, LAS GUÍAS ESTÁN PUBLICADAS EN https://guides.rubyonrails.org.

Callbacks de Active Record

Esta guía te enseña cómo engancharte en el ciclo de vida de tus objetos de Active Record.

Después de leer esta guía, sabrás:


1 El Ciclo de Vida del Objeto

Durante la operación normal de una aplicación Rails, los objetos pueden ser creados, actualizados y destruidos. Active Record proporciona ganchos en este ciclo de vida del objeto para que puedas controlar tu aplicación y sus datos.

Los callbacks te permiten activar lógica antes o después de un cambio en el estado de un objeto. Son métodos que se llaman en ciertos momentos del ciclo de vida de un objeto. Con los callbacks es posible escribir código que se ejecutará cada vez que un objeto de Active Record es inicializado, creado, guardado, actualizado, eliminado, validado o cargado desde la base de datos.

class BirthdayCake < ApplicationRecord
  after_create -> { Rails.logger.info("¡Felicidades, el callback se ha ejecutado!") }
end
irb> BirthdayCake.create
¡Felicidades, el callback se ha ejecutado!

Como verás, hay muchos eventos del ciclo de vida y múltiples opciones para engancharse en ellos, ya sea antes, después o incluso alrededor de ellos.

2 Registro de Callbacks

Para usar los callbacks disponibles, necesitas implementarlos y registrarlos. La implementación puede hacerse de múltiples maneras, como usando métodos ordinarios, bloques y procs, o definiendo objetos de callback personalizados usando clases o módulos. Vamos a repasar cada una de estas técnicas de implementación.

Puedes registrar los callbacks con un método de clase estilo macro que llama a un método ordinario para la implementación.

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

  before_validation :ensure_username_has_value

  private
    def ensure_username_has_value
      if username.blank?
        self.username = email
      end
    end
end

Los métodos de clase estilo macro también pueden recibir un bloque. Considera usar este estilo si el código dentro de tu bloque es tan corto que cabe en una sola línea:

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

  before_validation do
    self.username = email if username.blank?
  end
end

Alternativamente, puedes pasar un proc al callback para que sea activado.

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

  before_validation ->(user) { user.username = user.email if user.username.blank? }
end

Finalmente, puedes definir un objeto de callback personalizado, como se muestra a continuación. Cubriremos esto más adelante en detalle.

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

  before_validation AddUsername
end

class AddUsername
  def self.before_validation(record)
    if record.username.blank?
      record.username = record.email
    end
  end
end

2.1 Registrando Callbacks para Activarse en Eventos del Ciclo de Vida

Los callbacks también pueden ser registrados para activarse solo en ciertos eventos del ciclo de vida. Esto se puede hacer usando la opción :on y permite un control completo sobre cuándo y en qué contexto se activan tus callbacks.

NOTA: Un contexto es como una categoría o un escenario en el que deseas que se apliquen ciertas validaciones. Cuando validas un modelo de ActiveRecord, puedes especificar un contexto para agrupar validaciones. Esto te permite tener diferentes conjuntos de validaciones que se aplican en diferentes situaciones. En Rails, existen ciertos contextos predeterminados para las validaciones como :create, :update, y :save.

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

  before_validation :ensure_username_has_value, on: :create

  # :on también acepta un array
  after_validation :set_location, on: [ :create, :update ]

  private
    def ensure_username_has_value
      if username.blank?
        self.username = email
      end
    end

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

NOTA: Se considera una buena práctica declarar los métodos de callback como privados. Si se dejan públicos, pueden ser llamados desde fuera del modelo y violar el principio de encapsulación del objeto.

ADVERTENCIA. Abstente de usar métodos como update, save o cualquier otro método que cause efectos secundarios en el objeto dentro de tus métodos de callback.

Por ejemplo, evita llamar a update(attribute: "value") dentro de un callback. Esta práctica puede modificar el estado del modelo y potencialmente llevar a efectos secundarios imprevistos durante el commit.

En su lugar, puedes asignar valores directamente (por ejemplo, self.attribute = "value") en before_create, before_update, o callbacks anteriores para un enfoque más seguro.

3 Callbacks Disponibles

Aquí hay una lista con todos los callbacks de Active Record disponibles, listados en el orden en que serán llamados durante las operaciones respectivas:

3.1 Creando un Objeto

Consulta la sección after_commit / after_rollback para ver ejemplos usando estos dos callbacks.

Hay ejemplos a continuación que muestran cómo usar estos callbacks. Los hemos agrupado por la operación con la que están asociados, y finalmente mostramos cómo pueden ser usados en combinación.

3.1.1 Callbacks de Validación

Los callbacks de validación se activan cada vez que el registro es validado directamente a través del método valid? (o su alias validate) o invalid?, o indirectamente a través de create, update, o save. Se llaman antes y después de la fase de validación.

class User < ApplicationRecord
  validates :name, presence: true
  before_validation :titleize_name
  after_validation :log_errors

  private
    def titleize_name
      self.name = name.downcase.titleize if name.present?
      Rails.logger.info("Name titleized to #{name}")
    end

    def log_errors
      if errors.any?
        Rails.logger.error("Validation failed: #{errors.full_messages.join(', ')}")
      end
    end
end
irb> user = User.new(name: "", email: "john.doe@example.com", password: "abc123456")
=> #<User id: nil, email: "john.doe@example.com", created_at: nil, updated_at: nil, name: "">

irb> user.valid?
Name titleized to
Validation failed: Name can't be blank
=> false

3.1.2 Callbacks de Guardado

Los callbacks de guardado se activan cada vez que el registro es persistido (es decir, "guardado") en la base de datos subyacente, a través de los métodos create, update o save. Se llaman antes, después y alrededor del objeto que se guarda.

class User < ApplicationRecord
  before_save :hash_password
  around_save :log_saving
  after_save :update_cache

  private
    def hash_password
      self.password_digest = BCrypt::Password.create(password)
      Rails.logger.info("Password hashed for user with email: #{email}")
    end

    def log_saving
      Rails.logger.info("Saving user with email: #{email}")
      yield
      Rails.logger.info("User saved with email: #{email}")
    end

    def update_cache
      Rails.cache.write(["user_data", self], attributes)
      Rails.logger.info("Update Cache")
    end
end
irb> user = User.create(name: "Jane Doe", password: "password", email: "jane.doe@example.com")

Password encrypted for user with email: jane.doe@example.com
Saving user with email: jane.doe@example.com
User saved with email: jane.doe@example.com
Update Cache
=> #<User id: 1, email: "jane.doe@example.com", created_at: "2024-03-20 16:02:43.685500000 +0000", updated_at: "2024-03-20 16:02:43.685500000 +0000", name: "Jane Doe">

3.1.3 Callbacks de Creación

Los callbacks de creación se activan cada vez que el registro es persistido (es decir, "guardado") en la base de datos por primera vez — en otras palabras, cuando estamos guardando un nuevo registro, a través de los métodos create o save. Se llaman antes, después y alrededor del objeto que se crea.

class User < ApplicationRecord
  before_create :set_default_role
  around_create :log_creation
  after_create :send_welcome_email

  private
    def set_default_role
      self.role = "user"
      Rails.logger.info("User role set to default: user")
    end

    def log_creation
      Rails.logger.info("Creating user with email: #{email}")
      yield
      Rails.logger.info("User created with email: #{email}")
    end

    def send_welcome_email
      UserMailer.welcome_email(self).deliver_later
      Rails.logger.info("User welcome email sent to: #{email}")
    end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")

User role set to default: user
Creating user with email: john.doe@example.com
User created with email: john.doe@example.com
User welcome email sent to: john.doe@example.com
=> #<User id: 10, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe">

3.2 Actualizando un Objeto

Los callbacks de actualización se activan cada vez que un registro existente es persistido (es decir, "guardado") en la base de datos. Se llaman antes, después y alrededor del objeto que se actualiza.

ADVERTENCIA: El callback after_save se activa en las operaciones de creación y actualización. Sin embargo, se ejecuta consistentemente después de los callbacks más específicos after_create y after_update, independientemente del orden en que se realizaron las llamadas macro. De manera similar, los callbacks de guardado antes y alrededor siguen la misma regla: before_save se ejecuta antes de crear/actualizar, y around_save se ejecuta alrededor de las operaciones de creación/actualización. Es importante tener en cuenta que los callbacks de guardado siempre se ejecutarán antes/alrededor/después de los callbacks más específicos de creación/actualización.

Ya hemos cubierto los callbacks de validación y de guardado. Consulta la sección after_commit / after_rollback para ver ejemplos usando estos dos callbacks.

3.2.1 Callbacks de Actualización

class User < ApplicationRecord
  before_update :check_role_change
  around_update :log_updating
  after_update :send_update_email

  private
    def check_role_change
      if role_changed?
        Rails.logger.info("User role changed to #{role}")
      end
    end

    def log_updating
      Rails.logger.info("Updating user with email: #{email}")
      yield
      Rails.logger.info("User updated with email: #{email}")
    end

    def send_update_email
      UserMailer.update_email(self).deliver_later
      Rails.logger.info("Update email sent to: #{email}")
    end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "user" >

irb> user.update(role: "admin")
User role changed to admin
Updating user with email: john.doe@example.com
User updated with email: john.doe@example.com
Update email sent to: john.doe@example.com

3.2.2 Usando una Combinación de Callbacks

A menudo, necesitarás usar una combinación de callbacks para lograr el comportamiento deseado. Por ejemplo, es posible que desees enviar un correo electrónico de confirmación después de que se cree un usuario, pero solo si el usuario es nuevo y no se está actualizando. Cuando un usuario se actualiza, es posible que desees notificar a un administrador si se cambia información crítica. En este caso, puedes usar los callbacks after_create y after_update juntos.

class User < ApplicationRecord
  after_create :send_confirmation_email
  after_update :notify_admin_if_critical_info_updated

  private
    def send_confirmation_email
      UserMailer.confirmation_email(self).deliver_later
      Rails.logger.info("Confirmation email sent to: #{email}")
    end

    def notify_admin_if_critical_info_updated
      if saved_change_to_email? || saved_change_to_phone_number?
        AdminMailer.user_critical_info_updated(self).deliver_later
        Rails.logger.info("Notification sent to admin about critical info update for: #{email}")
      end
    end
end
irb> user = User.create(name: "John Doe", email: "john.doe@example.com")
Confirmation email sent to: john.doe@example.com
=> #<User id: 1, email: "john.doe@example.com", ...>

irb> user.update(email: "john.doe.new@example.com")
Notification sent to admin about critical info update for: john.doe.new@example.com
=> true

3.3 Destruyendo un Objeto

Los callbacks de destrucción se activan cada vez que se destruye un registro, pero se ignoran cuando se elimina un registro. Se llaman antes, después y alrededor del objeto que se destruye.

Encuentra ejemplos para usar after_commit / after_rollback.

3.3.1 Callbacks de Destrucción

class User < ApplicationRecord
  before_destroy :check_admin_count
  around_destroy :log_destroy_operation
  after_destroy :notify_users

  private
    def check_admin_count
      if admin? && User.where(role: "admin").count == 1
        throw :abort
      end
      Rails.logger.info("Checked the admin count")
    end

    def log_destroy_operation
      Rails.logger.info("About to destroy user with ID #{id}")
      yield
      Rails.logger.info("User with ID #{id} destroyed successfully")
    end

    def notify_users
      UserMailer.deletion_email(self).deliver_later
      Rails.logger.info("Notification sent to other users about user deletion")
    end
end
irb> user = User.find(1)
=> #<User id: 1, email: "john.doe@example.com", created_at: "2024-03-20 16:19:52.405195000 +0000", updated_at: "2024-03-20 16:19:52.405195000 +0000", name: "John Doe", role: "admin">

irb> user.destroy
Checked the admin count
About to destroy user with ID 1
User with ID 1 destroyed successfully
Notification sent to other users about user deletion

3.4 after_initialize y after_find

Cada vez que se instancia un objeto de Active Record, ya sea directamente usando new o cuando se carga un registro desde la base de datos, se llamará al callback after_initialize. Puede ser útil para evitar la necesidad de sobrescribir directamente tu método initialize de Active Record.

Cuando se carga un registro desde la base de datos, se llamará al callback after_find. after_find se llama antes de after_initialize si ambos están definidos.

NOTA: Los callbacks after_initialize y after_find no tienen contrapartes before_*.

Pueden registrarse al igual que los otros callbacks de Active Record.

class User < ApplicationRecord
  after_initialize do |user|
    Rails.logger.info("You have initialized an object!")
  end

  after_find do |user|
    Rails.logger.info("You have found an object!")
  end
end
irb> User.new
You have initialized an object!
=> #<User id: nil>

irb> User.first
You have found an object!
You have initialized an object!
=> #<User id: 1>

3.5 after_touch

El callback after_touch se llamará cada vez que se toque un objeto de Active Record. Puedes leer más sobre touch en la documentación de la API.

class User < ApplicationRecord
  after_touch do |user|
    Rails.logger.info("You have touched an object")
  end
end
irb> user = 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> user.touch
You have touched an object
=> true

Puede usarse junto con belongs_to:

class Book < ApplicationRecord
  belongs_to :library, touch: true
  after_touch do
    Rails.logger.info("A Book was touched")
  end
end

class Library < ApplicationRecord
  has_many :books
  after_touch :log_when_books_or_library_touched

  private
    def log_when_books_or_library_touched
      Rails.logger.info("Book/Library was touched")
    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 # triggers book.library.touch
A Book was touched
Book/Library was touched
=> true

4 Ejecutando Callbacks

Los siguientes métodos activan callbacks:

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

Además, el callback after_find se activa mediante los siguientes métodos de búsqueda:

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

El callback after_initialize se activa cada vez que se inicializa un nuevo objeto de la clase.

NOTA: Los métodos find_by_* y find_by_*! son buscadores dinámicos generados automáticamente para cada atributo. Aprende más sobre ellos en la sección de Buscadores dinámicos.

5 Callbacks Condicionales

Al igual que con las validaciones, también podemos hacer que la llamada de un método de callback sea condicional a la satisfacción de un predicado dado. Podemos hacer esto usando las opciones :if y :unless, que pueden tomar un símbolo, un Proc o un Array.

Puedes usar la opción :if cuando quieras especificar bajo qué condiciones el callback debe ser llamado. Si quieres especificar las condiciones bajo las cuales el callback no debe ser llamado, entonces puedes usar la opción :unless.

5.1 Usando :if y :unless con un Symbol

Puedes asociar las opciones :if y :unless con un símbolo que corresponde al nombre de un método predicado que se llamará justo antes del callback.

Cuando usas la opción :if, el callback no se ejecutará si el método predicado devuelve false; cuando usas la opción :unless, el callback no se ejecutará si el método predicado devuelve true. Esta es la opción más común.

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

Usando esta forma de registro también es posible registrar varios predicados diferentes que deben ser llamados para verificar si el callback debe ser ejecutado. Cubriremos esto en la sección Múltiples Condiciones de Callback.

5.2 Usando :if y :unless con un Proc

Es posible asociar :if y :unless con un objeto Proc. Esta opción es más adecuada cuando se escriben métodos de validación cortos, generalmente de una sola línea:

class Order < ApplicationRecord
  before_save :normalize_card_number,
    if: ->(order) { order.paid_with_card? }
end

Dado que el proc se evalúa en el contexto del objeto, también es posible escribir esto como:

class Order < ApplicationRecord
  before_save :normalize_card_number, if: -> { paid_with_card? }
end

5.3 Múltiples Condiciones de Callback

Las opciones :if y :unless también aceptan un array de procs o nombres de métodos como símbolos:

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

Puedes incluir fácilmente un proc en la lista de condiciones:

class Comment < ApplicationRecord
  before_save :filter_content,
    if: [:subject_to_parental_control?, -> { untrusted_author? }]
end

5.4 Usando Tanto :if como :unless

Los callbacks pueden mezclar tanto :if como :unless en la misma declaración:

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

El callback solo se ejecuta cuando todas las condiciones de :if y ninguna de las condiciones de :unless se evalúan como true.

6 Omitiendo Callbacks

Al igual que con las validaciones, también es posible omitir callbacks usando los siguientes métodos:

Consideremos un modelo User donde el callback before_save registra cualquier cambio en la dirección de correo electrónico del usuario:

class User < ApplicationRecord
  before_save :log_email_change

  private
    def log_email_change
      if email_changed?
        Rails.logger.info("Email changed from #{email_was} to #{email}")
      end
    end
end

Ahora, supongamos que hay un escenario en el que deseas actualizar la dirección de correo electrónico del usuario sin activar el callback before_save para registrar el cambio de correo electrónico. Puedes usar el método update_columns para este propósito:

irb> user = User.find(1)
irb> user.update_columns(email: 'new_email@example.com')

Lo anterior actualizará la dirección de correo electrónico del usuario sin activar el callback before_save.

ADVERTENCIA. Estos métodos deben usarse con precaución porque puede haber reglas de negocio importantes y lógica de aplicación en los callbacks que no deseas omitir. Omitirlos sin entender las posibles implicaciones puede llevar a datos inválidos.

7 Suprimir Callbacks

En ciertos escenarios, es posible que necesites evitar temporalmente que ciertos callbacks se ejecuten dentro de tu aplicación Rails. Esto puede ser útil cuando deseas omitir acciones específicas durante ciertas operaciones sin desactivar permanentemente los callbacks.

Rails proporciona un mecanismo para suprimir callbacks usando el módulo ActiveRecord::Suppressor. Al usar este módulo, puedes envolver un bloque de código donde deseas suprimir callbacks, asegurando que no se ejecuten durante esa operación específica.

Consideremos un escenario donde tenemos un modelo User con un callback que envía un correo electrónico de bienvenida a los nuevos usuarios después de que se registran. Sin embargo, puede haber casos en los que queramos crear un usuario sin enviar el correo electrónico de bienvenida, como durante la siembra de la base de datos con datos de prueba.

class User < ApplicationRecord
  after_create :send_welcome_email

  def send_welcome_email
    puts "Welcome email sent to #{self.email}"
  end
end

En este ejemplo, el callback after_create activa el método send_welcome_email cada vez que se crea un nuevo usuario.

Para crear un usuario sin enviar el correo electrónico de bienvenida, podemos usar el módulo ActiveRecord::Suppressor de la siguiente manera:

User.suppress do
  User.create(name: "Jane", email: "jane@example.com")
end

En el código anterior, el bloque User.suppress asegura que el callback send_welcome_email no se ejecute durante la creación del usuario "Jane", permitiéndonos crear el usuario sin enviar el correo electrónico de bienvenida.

ADVERTENCIA: Usar el Supresor de Active Record, aunque potencialmente beneficioso para controlar selectivamente la ejecución de callbacks, puede introducir complejidad y comportamientos inesperados. Suprimir callbacks puede oscurecer el flujo previsto de tu aplicación, lo que lleva a dificultades para entender y mantener el código a lo largo del tiempo. Considera cuidadosamente las implicaciones de suprimir callbacks, asegurando una documentación exhaustiva y pruebas reflexivas para mitigar los riesgos de efectos secundarios no deseados, problemas de rendimiento y fallas en las pruebas.

8 Detener la Ejecución

A medida que comienzas a registrar nuevos callbacks para tus modelos, se formará una cola para su ejecución. Esta cola incluirá todas las validaciones de tu modelo, los callbacks registrados y la operación de base de datos que se ejecutará.

Toda la cadena de callbacks está envuelta en una transacción. Si algún callback lanza una excepción, la cadena de ejecución se detiene y se emite un rollback, y el error se volverá a lanzar.

class Product < ActiveRecord::Base
  before_validation do
    raise "Price can't be negative" if total_price < 0
  end
end

Product.create # raises "Price can't be negative"

Esto rompe inesperadamente el código que no espera que métodos como create y save lancen excepciones.

NOTA: Si ocurre una excepción durante la cadena de callbacks, Rails la volverá a lanzar a menos que sea una excepción ActiveRecord::Rollback o ActiveRecord::RecordInvalid. En su lugar, deberías usar throw :abort para detener intencionalmente la cadena. Si algún callback lanza :abort, el proceso será abortado y create devolverá false.

class Product < ActiveRecord::Base
  before_validation do
    throw :abort if total_price < 0
  end
end

Product.create # => false

Sin embargo, lanzará un ActiveRecord::RecordNotSaved al llamar a create!. Esta excepción indica que el registro no se guardó debido a la interrupción del callback.

User.create! # => raises an ActiveRecord::RecordNotSaved

Cuando se llama a throw :abort en cualquier callback de destrucción, destroy devolverá false:

class User < ActiveRecord::Base
  before_destroy do
    throw :abort if still_active?
  end
end

User.first.destroy # => false

Sin embargo, lanzará un ActiveRecord::RecordNotDestroyed al llamar a destroy!.

User.first.destroy! # => raises an ActiveRecord::RecordNotDestroyed

9 Callbacks de Asociación

Los callbacks de asociación son similares a los callbacks normales, pero se activan por eventos en el ciclo de vida de la colección asociada. Hay cuatro callbacks de asociación disponibles:

  • before_add
  • after_add
  • before_remove
  • after_remove

Puedes definir callbacks de asociación agregando opciones a la asociación.

Supongamos que tienes un ejemplo donde un autor puede tener muchos libros. Sin embargo, antes de agregar un libro a la colección de autores, deseas asegurarte de que el autor no haya alcanzado su límite de libros. Puedes hacer esto agregando un callback before_add para verificar el límite.

class Author < ApplicationRecord
  has_many :books, before_add: :check_limit

  private
    def check_limit
      if books.count >= 5
        errors.add(:base, "Cannot add more than 5 books for this author")
        throw(:abort)
      end
    end
end

Si un callback before_add lanza :abort, el objeto no se agrega a la colección.

A veces puede que desees realizar múltiples acciones en el objeto asociado. En este caso, puedes apilar callbacks en un solo evento pasándolos como un array. Además, Rails pasa el objeto que se está agregando o eliminando al callback para que lo uses.

class Author < ApplicationRecord
  has_many :books, before_add: [:check_limit, :calculate_shipping_charges]

  def check_limit
    if books.count >= 5
      errors.add(:base, "Cannot add more than 5 books for this author")
      throw(:abort)
    end
  end

  def calculate_shipping_charges(book)
    weight_in_pounds = book.weight_in_pounds || 1
    shipping_charges = weight_in_pounds * 2

    shipping_charges
  end
end

De manera similar, si un callback before_remove lanza :abort, el objeto no se elimina de la colección.

NOTA: Estos callbacks se llaman solo cuando los objetos asociados se agregan o eliminan a través de la colección de asociaciones.

# Activa el callback `before_add`
author.books << book
author.books = [book, book2]

# No activa el callback `before_add`
book.update(author_id: 1)

10 Callbacks de Asociación en Cascada

Los callbacks pueden realizarse cuando cambian los objetos asociados. Funcionan a través de las asociaciones del modelo, donde los eventos del ciclo de vida pueden hacer cascada en las asociaciones y activar callbacks.

Supongamos un ejemplo donde un usuario tiene muchos artículos. Los artículos de un usuario deben ser destruidos si el usuario es destruido. Agreguemos un callback after_destroy al modelo User a través de su asociación con el modelo Article:

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

class Article < ApplicationRecord
  after_destroy :log_destroy_action

  def log_destroy_action
    Rails.logger.info("Article destroyed")
  end
end
irb> user = User.first
=> #<User id: 1>
irb> user.articles.create!
=> #<Article id: 1, user_id: 1>
irb> user.destroy
Article destroyed
=> #<User id: 1>

ADVERTENCIA: Al usar un callback before_destroy, debe colocarse antes de las asociaciones dependent: :destroy (o usar la opción prepend: true), para asegurar que se ejecuten antes de que los registros sean eliminados por dependent: :destroy.

11 Callbacks Transaccionales

11.1 after_commit y after_rollback

Dos callbacks adicionales se activan por la finalización de una transacción de base de datos: after_commit y after_rollback. Estos callbacks son muy similares al callback after_save excepto que no se ejecutan hasta que los cambios en la base de datos han sido confirmados o revertidos. Son más útiles cuando tus modelos de Active Record necesitan interactuar con sistemas externos que no son parte de la transacción de la base de datos.

Considera un modelo PictureFile que necesita eliminar un archivo después de que el registro correspondiente sea destruido.

class PictureFile < ApplicationRecord
  after_destroy :delete_picture_file_from_disk

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

Si algo lanza una excepción después de que se llame al callback after_destroy y la transacción se revierte, entonces el archivo habrá sido eliminado y el modelo quedará en un estado inconsistente. Por ejemplo, supongamos que picture_file_2 en el código a continuación no es válido y el método save! lanza un error.

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

Usando el callback after_commit podemos tener en cuenta este 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: La opción :on especifica cuándo se activará un callback. Si no proporcionas la opción :on, el callback se activará para cada evento del ciclo de vida. Lee más sobre :on.

Cuando una transacción se completa, los callbacks after_commit o after_rollback se llaman para todos los modelos creados, actualizados o destruidos dentro de esa transacción. Sin embargo, si una excepción se lanza dentro de uno de estos callbacks, la excepción burbujeará y cualquier método after_commit o after_rollback restante no se ejecutará.

class User < ActiveRecord::Base
  after_commit { raise "Intentional Error" }
  after_commit {
    # Esto no se llamará porque el anterior after_commit lanza una excepción
    Rails.logger.info("Esto no se registrará")
  }
end

ADVERTENCIA. Si tu código de callback lanza una excepción, necesitarás capturarla y manejarla dentro del callback para permitir que otros callbacks se ejecuten.

after_commit hace garantías muy diferentes a after_save, after_update y after_destroy. Por ejemplo, si una excepción ocurre en un after_save, la transacción se revertirá y los datos no se persistirán.

class User < ActiveRecord::Base
  after_save do
    # Si esto falla, el usuario no se guardará.
    EventLog.create!(event: "user_saved")
  end
end

Sin embargo, durante after_commit los datos ya fueron persistidos en la base de datos, por lo que cualquier excepción no revertirá nada más.

class User < ActiveRecord::Base
  after_commit do
    # Si esto falla, el usuario ya fue guardado.
    EventLog.create!(event: "user_saved")
  end
end

El código ejecutado dentro de los callbacks after_commit o after_rollback no está encerrado dentro de una transacción.

En el contexto de una sola transacción, si representas el mismo registro en la base de datos, hay un comportamiento crucial en los callbacks after_commit y after_rollback a tener en cuenta. Estos callbacks se activan solo para el primer objeto del registro específico que cambia dentro de la transacción. Otros objetos cargados, a pesar de representar el mismo registro de base de datos, no tendrán sus respectivos callbacks after_commit o after_rollback activados.

class User < ApplicationRecord
  after_commit :log_user_saved_to_db, on: :update

  private
    def log_user_saved_to_db
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create
irb> User.transaction { user.save; user.save }
# User was saved to database

ADVERTENCIA: Este comportamiento matizado es particularmente impactante en escenarios donde esperas una ejecución de callback independiente para cada objeto asociado con el mismo registro de base de datos. Puede influir en el flujo y la previsibilidad de las secuencias de callbacks, llevando a posibles inconsistencias en la lógica de la aplicación después de la transacción.

11.2 Alias para after_commit

Usar el callback after_commit solo en crear, actualizar o eliminar es común. A veces también es posible que desees usar un solo callback tanto para create como para update. Aquí hay algunos alias comunes para estas operaciones:

Vamos a repasar algunos ejemplos:

En lugar de usar after_commit con la opción on para una destrucción como a continuación:

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

Puedes en su lugar usar el after_destroy_commit.

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

Lo mismo se aplica para after_create_commit y after_update_commit.

Sin embargo, si usas el callback after_create_commit y el after_update_commit con el mismo nombre de método, solo permitirá que el último callback definido tenga efecto, ya que ambos internamente se alias a after_commit que sobrescribe los callbacks definidos anteriormente con el mismo nombre 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
      # Esto solo se llama una vez
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create # no imprime nada

irb> user.save # actualizando @user
User was saved to database

En este caso, es mejor usar after_save_commit en su lugar, que es un alias para usar el callback after_commit tanto para crear como para actualizar:

class User < ApplicationRecord
  after_save_commit :log_user_saved_to_db

  private
    def log_user_saved_to_db
      Rails.logger.info("User was saved to database")
    end
end
irb> user = User.create # creando un Usuario
User was saved to database

irb> user.save # actualizando usuario
User was saved to database

11.3 Orden de Callbacks Transaccionales

Por defecto (desde Rails 7.1), los callbacks de transacción se ejecutarán en el orden en que se definen.

class User < ActiveRecord::Base
  after_commit { Rails.logger.info("esto se llama primero") }
  after_commit { Rails.logger.info("esto se llama segundo") }
end

Sin embargo, en versiones anteriores de Rails, al definir múltiples callbacks transaccionales after_ (after_commit, after_rollback, etc.), el orden en que se ejecutaban los callbacks era inverso.

Si por alguna razón aún deseas que se ejecuten en orden inverso, puedes establecer la siguiente configuración en false. Los callbacks entonces se ejecutarán en el orden inverso. Consulta las opciones de configuración de Active Record para más detalles.

config.active_record.run_after_transaction_callbacks_in_order_defined = false

NOTA: Esto se aplica a todas las variaciones after_*_commit, como after_destroy_commit.

12 Objetos de Callback

A veces, los métodos de callback que escribirás serán lo suficientemente útiles como para ser reutilizados por otros modelos. Active Record hace posible crear clases que encapsulan los métodos de callback, para que puedan ser reutilizados.

Aquí hay un ejemplo de una clase de callback after_commit para tratar con la limpieza de archivos descartados en el sistema de archivos. Este comportamiento puede no ser único para nuestro modelo PictureFile y es posible que queramos compartirlo, por lo que es una buena idea encapsular esto en una clase separada. Esto hará que probar ese comportamiento y cambiarlo sea mucho más fácil.

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

Cuando se declara dentro de una clase, como arriba, los métodos de callback recibirán el objeto del modelo como un parámetro. Esto funcionará en cualquier modelo que use la clase de esta manera:

class PictureFile < ApplicationRecord
  after_commit FileDestroyerCallback.new
end

Ten en cuenta que necesitábamos instanciar un nuevo objeto FileDestroyerCallback, ya que declaramos nuestro callback como un método de instancia. Esto es particularmente útil si los callbacks hacen uso del estado del objeto instanciado. A menudo, sin embargo, tendrá más sentido declarar los callbacks como métodos de clase:

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

Cuando el método de callback se declara de esta manera, no será necesario instanciar un nuevo objeto FileDestroyerCallback en nuestro modelo.

class PictureFile < ApplicationRecord
  after_commit FileDestroyerCallback
end

Puedes declarar tantos callbacks como desees dentro de tus objetos de callback.


Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.