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
before_validation
after_validation
before_save
around_save
before_create
around_create
after_create
after_save
after_commit
/after_rollback
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.
before_validation
after_validation
before_save
around_save
before_update
around_update
after_update
after_save
after_commit
/after_rollback
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:
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
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.