#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)
L’episodio 193 [guardalo, leggilo] ha trattato i modelli completamente non persistenti. In quell’episodio abbiamo creato un modello che usava alcune delle funzionalità di ActiveRecord, ma che non aveva una tabella sul database corrispondente alle spalle. La tecnica utilizzata era un po’ forzata, dal momento che si tentava di fare un qualcosa per cui ActiveRecord non era stato progettato, ma ora, in Rails 3.0, abbiamo a disposizione una nuova funzionalità chiamata ActiveModel, che rende molto più semplice realizzare cose del genere.
Prima di entrare nei dettagli di ActiveModel, diamo una breve descrizione della parte dell’applicazione che cambieremo per usarlo.
Lo screenshot di sopra mostra una form per l’inserimento di un contatto, creata usando lo scaffolding. L’applicazione ha un modello chiamato Message
che attualmente viene reso persistente da ActiveRecord, il che significa che stiamo gestendo i messaggi attraverso il database. Cambiamo il modo in cui funziona questa form, in modo tale che invii semplicemente le email e non salvi i messaggi su una tabella del database.
Quando si pensa di fare una cosa del genere, è sempre una buona idea analizzare preventivamente i requisiti ed assicurarsi che non si voglia realmente salvare alcun dato della form sul database, pochè spesso ci sono degli effetti collaterali nel fare in questo modo. Un database può fare da backup e rende anche più semplice lo spostamento dei messaggi inviati in una coda di un processo di background. Per gli scopi di questo esempio, tuttavia, non vogliamo nessuna di quelle funzionalità, per cui siamo liberi di andare avanti e crearci il nostro modello non persistente.
Il codice della classe del messaggio appare così:
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
Message
eredita da ActiveRecord::Base
come ci aspetteremmo, ma dal momento che non vogliamo che questo modello abbia un corrispettivo sul database di backend, rimuoviamo questa discendenza. Nel momento in cui facciamo questa modifica, tuttavia, la nostra form smetterà di funzionare, poichè i validatori sono forniti da ActiveRecord. Fortunatamente, possiamo ripristinare questa funzionalità usando ActiveModel.
Se diamo un’occhiata al codice sorgente di Rails 3, vedremo che ci sono le cartelle activerecord
e activemodel
. Il team di sviluppo del core di Rails ha estratto da ActiveRecord tutto ciò che non fosse prettamente legato al database e l’ha spostato in ActiveModel. ActiveRecord continua ancora ad affidarsi pesantemente su ActiveModel per tutte le funzionalità che non sono specifiche del database e dal momento che ActiveModel è funzionalmente completo e ben testato, è adatto anche ad essere utilizzato al di fuori di ActiveRecord.
Se diamo un’occhiata alla cartella che contiene il codice di ActiveModel, potremo vedere i servizi che questi mette a disposizione:
Possiamo notare dall’elenco di sopra che ActiveModel include il codice di gestione delle callback, del tracciamento delle modifiche, della serializzazione e della validazione, insieme ad altre cose. L’ultima fra queste è proprio ciò che stavamo cercando.
Il codice per le validazioni ha il seguente commento vicino alla cima e possiamo vedere da quello che è piuttosto semplice aggiungere le validazioni a un modello. Tutto ciò di cui abbiamo bisogno è di includere il module Validations
e fornire i metodi getter per gli attributi su cui desideriamo chiamare i validatori:
# == Active Model Validations # # Provides a full validation framework to your objects. # # A minimal implementation could be: # # 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 #
Ora che sappiamo questa cosa, possiamo applicarla al nostro modello 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
Non è comunque abbastanza per far sì che il nostro modello si comporti come il controller si aspetta che faccia. Ci sono due problemi nel metodo create
. Innanzitutto, la chiamata a Message.new
non funzionerà poichè il nostro modello Message
non ha più un initializer che accetti un hash di attributi per argomento. In secondo luogo, save
non funzionerà dal momento che non esiste un backend sul database in cui salvare il nuovo messaggio.
class MessagesController < ApplicationController def new @message = Message.new end def create @message = Message.new(params[:message]) if @message.save # TODO qui si deve inviare il messaggio flash[:notice] = "Message sent! Thank you for contacting us." redirect_to root_url else render :action => 'new' end end end
Per primo, risolviamo il secondo problema. benchè non si possa salvare un messaggio, possiamo tuttavia controllarne la validità, per cui sostituiamo @message.save
con @message.valid?
:
def create @message = Message.new(params[:message]) if @message.valid? # TODO qui si deve inviare il messaggio flash[:notice] = "Message sent! Thank you for contacting us." redirect_to root_url else render :action => 'new' end end
Possiamo risolvere il primo problema, invece, scrivendo un metodo initialize
nella classe Message
che accetti un hash come parametro. Questo metodo itererà su ciascun elemento nell’hash e ne assegnerà il valore all’opportuno attributo del messaggio, usando il metodo 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
Se ora ricarichiamo la form, tuttavia, vedremo che non ci siamo ancora:
Questa volta l’errore è causato dalla mancanza del metodo to_key
nel modello Message
. L’errore viene lanciato dal metodo form_for
, per cui sembra che Rails stesso si aspetti che il nostro modello abbia una funzionalità che ancora non supporta. Aggiungiamogliela ora.
Anzichè provare a indovinare tutto ciò che Rails si aspetterà di vedere implementato dal nostro modello, c’è un simpatico lint test incluso in ActiveModel che ci permette di controllare se il nostro modello personalizzato si comporti o meno come Rails si aspetta che faccia. Se includiamo il module ActiveModel::Lint::Tests
in un test per il modello, esso controllerà che il modello abbia tutte le funzionalità necessarie.
Il codice sorgente per il module Lint::Tests
mostra i metodi ai quali il modello deve poter rispondere al fine di funzionare come si deve, incluso il to_key
. Possiamo fare in modo che il nostro modello funzioni includendo un paio di module ActiveRecord. Il primo di questi è Conversion
, che fornisce il metodo to_key ed alcuni altri. L’altro module è Naming
, ma in questo caso lo estendiamo nella nostra classe anzichè includerlo, poichè include alcuni metodi di classe.
Oltre a includere il module Conversion
, dobbiamo anche definire un metodo persisted?
, che dovrà restituire sempre false
, dato che il nostro modello non viene mai reso persistente sul database. Con queste modifiche, ora la nostra classe Message
appare così:
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
Se ricarichiamo la form, ora, riprenderà a funzionare, ad indicare che ora il modello Message
soddisfa tutti i requisiti richiesti da Rails 3. Se proviamo a fare il submit della form, vedremo che anche i validatori funzionano:
Abbiamo trattato solo una piccola parte di ciò che mette a disposizione ActiveModel in questo episodio, ma dovrebbe essere abbastanza per stimolare il vostro appetito e darvi una buona ragione per approfondire sul codice sorgente per vedere cosa si può fare. Il codice è ben documentato e strutturato, per cui se troverete qualcosa di utile, ci dovrebbero essere anche le informazioni sufficienti nei commenti per mettervi in grado di utilizzare la funzionalità trovata.