#219 Active Model
- Download:
- source codeProject Files in Zip (92.6 KB)
- mp4Full Size H.264 Video (12.7 MB)
- m4vSmaller H.264 Video (8.03 MB)
- webmFull Size VP8 Video (20.9 MB)
- ogvFull Size Theora Video (18.2 MB)
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:
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:
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.
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
.
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.
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?
.
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
.
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.
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
:
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:
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.