spacer ASCIIcasts

Tags

219: Active Model 

(view original Railscast)

Other translations: spacer spacer spacer spacer

Written by Juan Lupión

El episodio 193 [verlo, leerlo] trataba de los modelos sin tablas. En ese episodio creamos un modelo que utilizaba algunas de las características de ActiveRecord pero que no tenía una base de datos por detrás. Podría decirse que esta técnica era un hack, dado que no era algo para lo que ActiveRecord estuviera diseñado pero ahora en Rails 3.0 tenemos una nueva funcionalidad llamada ActiveModel con la que se pueden hacer fácilmente este tipo de cosas.

Antes de entrar en detalle con ActiveModel vamos a describir la sección de la aplicación que vamos a modificar:

spacer

En la captura de pantalla anterior vemos un formulario de contacto que ha sido creado utilizando el andamiaje de Rails. La aplicación tiene un modelo llamado Message y que ActiveRecord se encarga de guardar en la base de datos. Vamos a cambiar este comportamiento para que tan sólo envíe el correo electrónico sin almacenar nada en ninguna tabla.

Por supuesto, siempre que pensemos acometer este tipo de soluciones es buena idea plantearnos nuestros requerimientos y asegurarnos de que realmente no nos interesa almacenar estos datos en la base de datos porque por lo general sí que nos interesa hacerlo: en primer lugar la base de datos puede actuar de copia de respaldo y hace que sea más fácil pasar el envío de mensajes a una cola de tareas en un proceso background. Como no vamos a necesitar nada de esto en nuestro ejemplo, vamos a seguir adelante y haremos que este sea un modelo sin tabla.

El código de la clase Message tiene el siguiente aspecto:

/app/models/message.rb

class Message < ActiveRecord::Base
 validates_presence_of :name
 validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
 validates_length_of :content, :maximum => 500
end

Como es de esperar de una clase de modelo, Message hereda de ActiveRecord::Base, herencia que tendremos que romper porque no queremos que este modelo esté respaldado por una base de datos. En cuanto lo hagamos el formulario dejará de funcionar porque es ActiveRecord quien está haciendo las validaciones. Por suerte podemos recuperar esta funcionalidad utilizando ActiveModel.

Si echamos un vistazo al código fuente de Rails 3 veremos que hay un par de directorios, activerecord y activemodel. El equipo de Rails ha extraído de ActiveRecord todo lo que no era específico del soporte de base de datos y lo ha puesto en ActiveModel, cuyas funcionalidades están muy bien probadas y es ideal para ser usado fuera de ActiveRecord (si bien ActiveRecord se apoya en ActiveModel para todo lo que no tiene que ver con la base de datos).

Si echamos un vistazo al directorio que contiene el código de ActiveModel podemos ver toda la funcionalidad que suministra.

spacer

Tal y como puede verse del listado anterior ActiveModel incluye entre otras cosas el código para gestionar callbacks, serialización y validación. Esto último es justamente lo que estábamos buscando.

En el código para las validaciones aparece el siguiente comentario al principio, de donde podemos ver que es muy fácil añadir validaciones a cualquier modelo. Lo único que tenemos que hacer es incluir el módulo Validations y proporcionar métodos de acceso para los atributos sobre los que vamos a aplicar validadores.

  # == Validaciones de ActiveModel
  #   
  # Suministra un marco completo de validación a tus objetos
  # 
  # Una implementación mínima sería
  # 
  #   class Person
  #     include ActiveModel::Validations
  # 
  #     attr_accessor :first_name, :last_name
  #
  #     validates_each :first_name, :last_name do |record, attr, value|
  #       record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
  #     end
  #   end
  # 

Una vez que sabemos esto podemos aplicarlo a nuestro modelo Message.

/app/models/message.rb

class Message
  include ActiveModel::Validations
  
  attr_accessor :name, :email, :content
 
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
end

Pero con esto no es suficiente para que nuestro modelo se comporte como espera el controlador, aún tenemos dos problemas en el método create. Primero, la llamada a Message.new no funciona porque el modelo Message ya no tiene un inicializador que reciba el hash de atributos como argumento. Y en segundo lugar el método save no funcionará porque no tenemos un servidor de base de datos en el que almacenar el nuevo mensaje.

/apps/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def new
    @message = Message.new
  end

  def create
    @message = Message.new(params[:message])
    if @message.save
      # TODO send message here
      flash[:notice] = "Message sent! Thank you for contacting us."
      redirect_to root_url
    else
      render :action => 'new'
    end
  end
end

Vamos a empezar corrigiendo el segundo de estos problemas. Si bien no podemos guardar el mensaje sí que podemos comprobar si es válido, así que vamos a cambiar @message.save con @message.valid?.

/app/controllers/messages_controllers.rb

def create
  @message = Message.new(params[:message])
  if @message.valid?
    # TODO send message here
    flash[:notice] = "Message sent! Thank you for contacting us."
    redirect_to root_url
  else
    render :action => 'new'
  end
end

Y para arreglar el primer problema, vamos a escribir un método initialize en el modelo Message que recibirá un hash como parámetro. Este método recorrerá cada elemento de este hash y asignará su valor al atributo correspondiente, utilizando el método send.

/app/models/message.rb

class Message
  include ActiveModel::Validations
  
  attr_accessor :name, :email, :content
 
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
  
  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end
end

Si recargamos el formulario veremos que aún nos falta un poco más para terminar.

spacer

Esta vez el error viene provocado porque al modelo Message le falta el método to_key. El error ha sido devuelto por el método form_for, así que esto parece indicar que Rails espera que nuestro modelo tenga cierta funcionalidad que aún no soporta. Añadámosla ahora.

En vez de tener que adivinar todo lo que Rails espera que nuestro modelo tenga, podemos usar una herramienta de chequeo incluida con ActiveModel que nos permite comprobar si nuestro modelo cumple con todo lo que Rails espera de él. Si incluimos el módulo ActiveModel::Lint::Tests en un test del modelo, comprobará que éste tiene toda la funcionalidad necesaria.

El código fuente del módulo Lint::Tests muestra los métodos a los que el método tiene que responder para que todo funcione como debería (incluyendo to_key). Por tanto podemos hacer que nuestro modelo funcione incluyendo un par de módulos de ActiveModel. El primero de ellos es Conversion, y es el que proporciona el método to_key y algunos más. El otro módulo se llama Naming pero en este caso en lugar de incluirlo en nuestra clase tendremos que usar extend porque incluye algunos métodos de clase.

Además de incluir el módulo Conversion tenemos que definir el método persisted? en nuestro modelo, que tiene que devolver false dado que no estamos guardando nuestro modelo en la base datos. Con estos cambios, este es el aspecto que tiene nuestro modelo Message:

/app/models/message.rb

class Message
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  
  attr_accessor :name, :email, :content
 
  validates_presence_of :name
  validates_format_of :email, :with => /^[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}$/i
  validates_length_of :content, :maximum => 500
  
  def initialize(attributes = {})
    attributes.each do |name, value|
      send("#{name}=", value)
    end
  end
  
  def persisted?
    false
  end
end

Si recargamos el formulario volverá a funcionar de nuevo, lo que significa que el modelo Message ya satisface todos lo que Rails 3 requiere de un modelo. Si intentamos enviar el formulario veremos que los validadores ya están funcionando:

spacer

En este episodio hemos visto sólo un poco de todo lo que ActiveModel nos ofrece, pero debería ser suficiente para despertar nuestro interés y darnos una razón para examinar su documentación y aprender todo lo que puede hacer. El código está bien documentado y estructurado, así que si vemos algo que nos parece interesante seguro que encontraremos suficiente información en los comentarios del archivo como para poder ponernos manos a la obra.

Previous episode Next episode
gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.