Inicio / Ruby / Ruby on Rails 8: Desarrollo Fullstack / Validaciones y Callbacks

Validaciones y Callbacks

presence, uniqueness, format, custom validations, errors, before_save y callbacks.

Intermedio Bases de datos
🔒 Solo lectura
📖

Estás en modo lectura

Puedes leer toda la lección, pero para marcar progreso, hacer ejercicios y ganar XP necesitas una cuenta Pro.

Desbloquear por $9/mes

Validaciones y Callbacks en Active Record

Las validaciones garantizan que solo datos válidos se guarden en la base de datos. Los callbacks permiten ejecutar lógica automáticamente en momentos clave del ciclo de vida de un modelo.


¿Por qué validar?

Sin validaciones, podrías guardar datos inconsistentes:

# ❌ Sin validaciones — se guarda un artículo sin título
Article.create(title: "", body: nil)

# ✅ Con validaciones — Rails rechaza datos inválidos
class Article < ApplicationRecord
  validates :title, presence: true
  validates :body,  presence: true
end

article = Article.new(title: "", body: nil)
article.valid?  # => false
article.save    # => false (no se guarda)
article.errors.full_messages
# => ["Title can't be blank", "Body can't be blank"]

Validaciones integradas

presence — campo no vacío

class Article < ApplicationRecord
  validates :title, presence: true
  validates :body,  presence: true
end

uniqueness — valor único

class User < ApplicationRecord
  validates :email, uniqueness: true

  # Case-insensitive
  validates :email, uniqueness: { case_sensitive: false }

  # Único dentro de un scope
  validates :slug, uniqueness: { scope: :category }
end

💡 Siempre complementa uniqueness con un índice único en la base de datos para evitar condiciones de carrera.

length — longitud del texto

class Article < ApplicationRecord
  validates :title, length: { minimum: 5 }
  validates :title, length: { maximum: 200 }
  validates :title, length: { in: 5..200 }

  validates :summary, length: {
    maximum: 500,
    too_long: "no puede exceder los %{count} caracteres"
  }

  validates :password, length: { is: 8 }  # exactamente 8 caracteres
end

format — expresión regular

class User < ApplicationRecord
  validates :email, format: {
    with: /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i,
    message: "no tiene un formato válido"
  }

  validates :username, format: {
    with: /\A[a-zA-Z0-9_]+\z/,
    message: "solo permite letras, números y guiones bajos"
  }
end

numericality — valores numéricos

class Product < ApplicationRecord
  validates :price, numericality: true                        # es un número
  validates :price, numericality: { greater_than: 0 }         # mayor que 0
  validates :stock, numericality: { only_integer: true }      # solo enteros
  validates :discount, numericality: {
    greater_than_or_equal_to: 0,
    less_than_or_equal_to: 100
  }
end

inclusion y exclusion

class Article < ApplicationRecord
  validates :status, inclusion: {
    in: %w[draft published archived],
    message: "%{value} no es un estado válido"
  }

  validates :category, exclusion: {
    in: %w[spam test],
    message: "la categoría %{value} no está permitida"
  }
end

acceptance — aceptar términos

class User < ApplicationRecord
  validates :terms_of_service, acceptance: true
end

confirmation — confirmar campo

class User < ApplicationRecord
  validates :email, confirmation: true
  # El usuario debe llenar email y email_confirmation
end

Opciones comunes de validación

class Article < ApplicationRecord
  # Solo validar al crear
  validates :slug, uniqueness: true, on: :create

  # Solo validar al actualizar
  validates :reason, presence: true, on: :update

  # Validar condicionalmente
  validates :body, presence: true, if: :published?
  validates :draft_note, presence: true, unless: :published?

  # Con un proc
  validates :special_field, presence: true, if: -> { category == "premium" }

  # Permitir nil o blank
  validates :website, format: { with: /\Ahttps?:\/\/.*\z/ }, allow_blank: true
  validates :age, numericality: true, allow_nil: true

  private

  def published?
    status == "published"
  end
end

Validaciones personalizadas

Método validate

class Article < ApplicationRecord
  validate :title_cannot_contain_spam
  validate :publish_date_in_future, on: :create

  private

  def title_cannot_contain_spam
    if title.present? && title.downcase.include?("spam")
      errors.add(:title, "no puede contener palabras prohibidas")
    end
  end

  def publish_date_in_future
    if publish_date.present? && publish_date < Date.today
      errors.add(:publish_date, "debe ser una fecha futura")
    end
  end
end

Validador personalizado como clase

# app/validators/email_domain_validator.rb
class EmailDomainValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.blank?

    domain = value.split("@").last
    blocked_domains = %w[tempmail.com throwaway.com]

    if blocked_domains.include?(domain)
      record.errors.add(attribute, options[:message] || "no permite dominios temporales")
    end
  end
end

# Uso en el modelo
class User < ApplicationRecord
  validates :email, email_domain: true
  # O con mensaje personalizado:
  validates :email, email_domain: { message: "usa un correo válido" }
end

El objeto errors

article = Article.new(title: "")
article.valid?  # => false

# Todos los errores
article.errors.full_messages
# => ["Title can't be blank", "Body can't be blank"]

# Errores de un campo específico
article.errors[:title]
# => ["can't be blank"]

# ¿Tiene errores en un campo?
article.errors.include?(:title)  # => true

# Agregar errores manualmente
article.errors.add(:base, "Ocurrió un error general")

# Cantidad de errores
article.errors.count  # => 3

# Limpiar errores
article.errors.clear

En las vistas puedes mostrar los errores:

<% if @article.errors.any? %>
  <div class="alert alert-danger">
    <h4><%= pluralize(@article.errors.count, "error") %> impiden guardar:</h4>
    <ul>
      <% @article.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

Callbacks del ciclo de vida

Los callbacks son métodos que se ejecutan automáticamente en momentos específicos:

Crear: before_validation → after_validation → before_save → around_save →
       before_create → around_create → after_create → after_save → after_commit

Actualizar: before_validation → after_validation → before_save → around_save →
            before_update → around_update → after_update → after_save → after_commit

Eliminar: before_destroy → around_destroy → after_destroy → after_commit

before_save

Se ejecuta antes de guardar (crear o actualizar):

class Article < ApplicationRecord
  before_save :generate_slug
  before_save :normalize_title

  private

  def generate_slug
    self.slug = title.parameterize if slug.blank?
  end

  def normalize_title
    self.title = title.strip.titleize
  end
end

before_create

Solo se ejecuta antes de crear un registro nuevo:

class User < ApplicationRecord
  before_create :assign_default_role
  before_create :generate_auth_token

  private

  def assign_default_role
    self.role ||= "member"
  end

  def generate_auth_token
    self.auth_token = SecureRandom.hex(20)
  end
end

after_create

Se ejecuta después de crear un registro:

class User < ApplicationRecord
  after_create :send_welcome_email
  after_create :create_default_settings

  private

  def send_welcome_email
    UserMailer.welcome(self).deliver_later
  end

  def create_default_settings
    create_setting(theme: "light", notifications: true)
  end
end

before_destroy

Se ejecuta antes de eliminar un registro:

class Article < ApplicationRecord
  before_destroy :check_if_deletable

  private

  def check_if_deletable
    if published? && comments.count > 0
      errors.add(:base, "No se puede eliminar un artículo publicado con comentarios")
      throw(:abort)  # Cancela la eliminación
    end
  end
end

around_* callbacks

Envuelven la operación, permitiendo ejecutar código antes y después:

class Article < ApplicationRecord
  around_save :log_changes

  private

  def log_changes
    Rails.logger.info "Antes de guardar: #{changes}"
    yield  # Ejecuta el save
    Rails.logger.info "Después de guardar: #{title} guardado correctamente"
  end
end

after_commit

Se ejecuta después de que la transacción de base de datos se confirme:

class Article < ApplicationRecord
  after_commit :notify_subscribers, on: :create
  after_commit :update_search_index, on: [:create, :update]
  after_commit :remove_from_search_index, on: :destroy

  private

  def notify_subscribers
    NotifySubscribersJob.perform_later(self)
  end

  def update_search_index
    SearchIndexJob.perform_later("update", self.class.name, id)
  end

  def remove_from_search_index
    SearchIndexJob.perform_later("delete", self.class.name, id)
  end
end

💡 Usa after_commit en lugar de after_save cuando necesites que la transacción esté completada (por ejemplo, para enviar trabajos a una cola).


Callbacks condicionales

class Article < ApplicationRecord
  before_save :notify_editor, if: :published?
  before_save :clear_cache,   unless: :draft?
  after_save  :log_update,    if: -> { saved_change_to_title? }

  private

  def notify_editor
    EditorMailer.article_published(self).deliver_later
  end

  def clear_cache
    Rails.cache.delete("articles/#{id}")
  end

  def log_update
    Rails.logger.info "Título cambiado a: #{title}"
  end
end

Saltar callbacks

A veces necesitas guardar sin ejecutar callbacks:

# Actualizar sin callbacks
article.update_column(:views_count, 100)
article.update_columns(views_count: 100, last_viewed_at: Time.current)

# Estos métodos NO ejecutan callbacks:
# update_column, update_columns, update_all
# delete, delete_all
# increment!, decrement!
# touch (depende de la versión)

Orden de los callbacks

Los callbacks se ejecutan en el orden en que se definen:

class Article < ApplicationRecord
  before_save :primero
  before_save :segundo
  before_save :tercero

  # Se ejecutan en orden: primero → segundo → tercero
end

Resumen

En esta lección aprendiste:

  • Las validaciones integradas: presence, uniqueness, length, format, numericality, inclusion
  • Opciones como on:, if:, unless:, allow_blank:
  • Cómo crear validaciones personalizadas con validate y clases validadoras
  • Cómo trabajar con el objeto errors para mostrar mensajes al usuario
  • Los callbacks del ciclo de vida: before_save, after_create, before_destroy, around_*, after_commit
  • Callbacks condicionales y cómo saltarlos cuando es necesario

En la siguiente lección exploraremos los controladores y acciones en detalle.

🔒

Ejercicio práctico disponible

Validaciones y callbacks personalizados

Desbloquear ejercicios
// Validaciones y callbacks personalizados
// Desbloquea Pro para acceder a este ejercicio
// y ganar +50 XP al completarlo

function ejemplo() {
    // Tu código aquí...
}

¿Te gustó esta lección?

Con Pro puedes marcar progreso, hacer ejercicios, tomar quizzes, ganar XP y obtener tu constancia.

Ver planes desde $9/mes