Ricordo ancora con piacere quel 23 Dicembre 2008 in cui David Heinemeier Hansson annuncio che il team di Merb si sarebbe fuso con quello di Rails per dare vita alla terza versione del framework. Da allora sono trascorsi più di 365 giorni e più di 4000 commits sul repository ufficiale di Ruby on Rails, 250 persone hanno contribuito ad un preciso e pragmatico refactoring che ha ottimizzato tutti gli aspetti carenti della versione precedente fino al rilascio della versione 3.0 beta del 5 Febbraio.
Ma cosa è cambiato in Rails 3.0?
Il maggior sforzo nella creazione di questa release è stato incentrato nel trasformare Rails da un lento ed interdipendente insieme di librerie ad un veloce e agnostico framework, capace di esporre un set di API che ogni componente implementa correttamente.
Prendiamo per esempio il criticato rapporto elitario tra ActiveRecord e ActionPack; in Rails 2.0 questi colloquiano senza nessun meccanismo di astrazione, l'una invocando metodi dell'altra, rendendo estremamente difficile la sostituzione di una delle due librerie (ad esempio DataMapper al posto di ActiveRecord).
Nella versione 3.0 del framework è stato saggiamente inserito un nuovo oggetto 'ActiveModel' che incorpora le specifiche di comunicazione e che qualsiasi ORM può includere ottenendo istantaneamente (o quasi) il privilegio di colloquio con ActionPack.
Non volendo ridurre l'articolo ad una sequenza di modifiche apportate a Rails 3.0 proviamo a sperimentarne le principali su di una piccola applicazione di prova; per fare questo procuriamoci in primis una copia funzionante della beta del framework digitando da linea di comando (è necessario Ruby 1.8.7):
gem install tzinfo builder i18n memcache-client rack rake rack-test rack-mount erubis mail text-format thor bundler gem install rails --pre
Concludiamo l'introduzione di questo articolo creando un applicazione Rails 3.0 col più classico set di comandi:
rails showcase_3_0
Uno degli aspetti più fastidiosi nella gestione di un applicazione Rails 2.x era la poca efficienza nel metodo di vendoring delle dipendenze; se ricordate l'elenco delle gemme richieste doveva essere effettuato all'interno del file 'config/environment.rb' con questa sintassi:
config.gem "bj" config.gem "aws-s3", :lib => "aws/s3"
il che portava inevitabilmente al problema 'cosa succede se una specifica gemma è necessaria prima che l'applicazione chiami il file environment.rb
?'.
Un esempio lampante in tal senso è costituito da 'rack', componente vitale al funzionamento del framework e necessario ben prima della routine di inizializzazione. La versione 3.0 di Rails risolve il problema delegando la gestione delle dipendenze ad un gemma: 'bundler' che a questo punto diventa l'unica che deve essere installata a priori sul computer.
Bundler legge le istruzioni relative a quali gemme installare all'interno del file Gemfile
presente nella cartella principale di ogni applicazione Rails 3.0; apriamo ad esempio quello di showcase_3_0
ed esaminiamone il contenuto:
# ho rimosso le istruzioni commentate
source 'http://gemcutter.org'
gem "rails", "3.0.0.beta"
gem "sqlite3-ruby", :require => "sqlite3"
appare chiaro che in questo momento le uniche dipendenze richieste sono verso la versione 3.0 del framework e l'adapter verso il database di default, sqlite.
Supponendo di aver ricevuto showcase_3_0
via email in un archivio compresso, tutto quello che dovremmo fare per allineare il nostro ambiente alle specifiche dell'applicazione è eseguire nella root della stessa i comandi:
bundle install
La gemma, seguendo le istruzioni del file appena esaminato, popolerebbe una cartella di convenienza (normalmente all'interno della home dell'utente che ha lanciato la procedura) con tutte le dipendenze richieste preoccupandosi di includere tale cartella nei percorsi di load dell'applicazione e sollevando noi da tediose procedure di setup.
Se invece dovessimo essere noi a dover distribuire quanto creato non dovremmo far altro che digitare:
bundle pack
per includere i singoli file .gem
che compongono l'albero delle dipendenze, specificato in Gemfile
, all'interno della cartella vendor/cache
dell'applicazione; tale percorso è infatti il primo ad essere scandagliato da bundler alla ricerca di gemme durante il comando install
.
Nella seconda parte dell'articolo: strutturare le query con l'algebra relazionale e le migliorie sotto il profilo RESTful.
Arel e l'algebra relazionale
Grandi novità per ActiveRecord 3.0: l'ORM abbandona la sua sintassi per evolversi verso una strutturazione ancora più flessibile ed intuitiva abbracciando Arel, una gemma che implementa il concetto di algebra relazionale. Ma, cos'è l'algebra relazionale?
Il nome 'algebra' ci riporta ad operatori che tutti conosciamo ed utilizziamo quotidianamente: parlo dei classici simboli +
, -
, /
, etc. che applicati al sistema dei numeri reali concorrono a definire la cosiddetta algebra elementare. Il termine algebra relazionale indica che in questo caso gli operatori insistono non sull'insieme R ma su quello delle relazioni; quindi l'algebra relazionale descrive una query come una successione di operazioni sull'insieme delle relazioni.
Prendiamo ad esempio il semplicissimo frammento SQL:
SELECT * FROM pals LEFT JOIN countries on countries.id = pals.country_id WHERE pals.age > 10
la sua controparte in algebra relazionale è la seguente:
Implementando questo meccanismo Arel rende di fatto possibile concatenare in modo intuitivo e trasparente un indefinito numero di operatori (selezione, join, ordinamento, etc.) sobbarcandosi il costo computazionale della loro traduzione in SQL.
Per sperimentare questa funzionalità creiamo gli oggetti Pal
e Country
in showcase_3_0
approfittando dell'occasione anche per notare i piccoli cambiamenti nella sintassi del comandi di generazione:
rails generate model Pal name:string country_id:integer rails generate model Country name:string
Modifichiamo ora il file db/seeds.rb
per specificare alcuni valori di default della tabella countries inserendo:
countries.create([{:name => 'Italy'},{:name => 'France'},{:name => 'Spain'}])
Definiamo la relazione (1, n)
tra countries
e pals
, agendo sui rispettivi file nella cartella app/models
come segue:
file country.rb
class Country < ActiveRecord::Base has_many :pals end
file pal.rb
class Pal < ActiveRecord::Base belongs_to :country end
Ora lanciamo i soliti comandi per la creazione ed il popolamento del database:
rake db:create && rake db:migrate && rake db:seed
A questo punto possiamo invocare la console e sperimentare un po' le possibilità offerte dalla nuove API di ActiveRecord:
rails console # per lanciare la console >> Pal.create[{:name=>'Sandro',:country_id=>1},{:name=>'Francesca',:country_id=>1},{:name=>'Alberto',:country_id=>2}] => [#<Pal id: 2, name: "Sandro", country_id: 1, created_at:... >> Pal.where(:country_id=>1) => #<ActiveRecord::Relation:0x1032350d8 @scope_for... >> Pal.where(:country_id=>1).all => [#<Pal id: 2, name: "Sandro", country_id: 1,... >> Pal.where(:country_id=>1).to_sql => "SELECT "pals".* FROM "pals" WHERE ("pals"."country_id" = 1)" >> Pal.joins(:country).where(:countries=>{:name=>'Italy'}).order(:name).all => [#<Pal id: 3, name: "Francesca", country_id: 1, created_at: ...
Il meccanismo di generazione delle query, beneficiando del supporto di Arel, consente di concatenare fra di loro un numero indefinito di operazioni; inoltre finché su questa catena non viene invocato un metodo di iterazione (es: each
) o di collezione (es: all
) l'oggetto di ritorno è sempre di tipo ActiveRecord::Relation
e quindi passibile di nuove concatenazioni.
Infine è possibile in ogni momento conoscere la forma dell'SQL risultante invocando sulla catena il metodo to_sql
.
Con questi piccoli esperimenti abbiamo esplorato una piccola porzione dei benefici di questa nuova sintassi che nei prossimi mesi renderà interrogare le basi di dati ancora più semplice e flessibile; per chi stesse effettuando un porting di una applicazione 2.x ricordo infine che il vecchio approccio è ancora utilizzabile anche se 'deprecated'.
More REST in Rails
Rails 3.0 apporta alcune sostanziali modifiche alla sintassi di routes
e renders
; lo scopo è quello di sfruttare ancora di più le convenzioni insite nel paradigma RESTful per ridurre le linee di codice necessarie. Capita spesso infatti che, scrivendo un applicazione con Rails 2.x, quasi ogni azione di ogni controller si concluda con un respond_to
più o meno complesso, come nell'esempio seguente:
def index @users = User.all respond_to do |format| format.html format.xml { render :xml => @users } format.json { render :json => @users } end end
Per ovviare a questo la nuova versione del framework introduce il metodo respond_with
che non fa altro che applicare un comportamento convenzionale basandosi su alcuni fattori quali il verbo con cui è stata fatta la richiesta, la presenza o meno di errori, il tipo di formato di output richiesto, etc. Ad esempio il codice esposto poc'anzi si traduce in:
def index @users = User.all respond_with(@users) end
mentre un'azione di creazione classica può essere implementata in questo modo:
def create @user = User.new(params[:user]) flash[:notice] = 'Utente creato correttamente' if @user.save respond_with(@user) end
Notate come le due chiamate che concludono le due funzioni siano esattamente le stesse: respond_with(@user)
; la magia avviene infatti all'interno della classe ActionController::Responder
, invocata dalla funzione respond_with
, dove metodi specifici per ogni formato intervengono utilizzando convenzioni per stabilire la corretta reazione a fronte delle informazioni disponibili. Prendiamo per esempio il classico formato HTML e ispezioniamone il funzionamento:
def to_html default_render rescue ActionView::MissingTemplate => e navigation_behavior(e) end def navigation_behavior(error) if get? raise error elsif has_errors? && default_action render :action => default_action else redirect_to resource_location end end
la funzione to_html
tenta in primis di eseguire il template associato all'azione corrente, se fallisce (ad esempio perchè non esiste un template, come nelle azioni di create) invoca la funzione navigation_behavior
che si comporta nel seguente modo:
- se il verbo di chiamata è
GET
viene lanciata un eccezione (in quanto ogni azione chiamata in get dovrebbe tradursi nel rendering di un template a video) - se invece ci sono degli errori all'interno della risorsa (nel nostro caso @user) viene effettuato il render dell'azione di default (
edit.html.erb
per il verboPUT
enew.html.erb
per ilPOST
) - nel caso in cui nessuna delle due condizioni precedenti sia soddisfatta lo script esegue un redirect verso l'azione
index
del controller corrente
Chiaramente è possibile modificare questa convenzione, ad esempio operando come nell'esempio seguente, che riprende il codice scritto in precedenza forzando però il redirect verso una pagina a nostro piacere (nel caso di formato HTML):
def create @user = User.new(params[:user]) flash[:notice] = 'Utente creato correttamente' if @user.save respond_with(@user) do |format| format.html { redirect_to users_url } end end
Candies
Ci sono ancora tantissime cose di cui non ho fatto parola che sono cambiate in questa nuova release; tra queste le due principali si chiamano SafeBuffer
e ActiveModel
. Supponiamo di voler includere in una vista il seguente frammento di codice:
<%= "Saluto <b>tutti</b> i lettori di html.it" %>
Eseguendolo inaspettatamente otterremmo questo risultato:
Saluto <b>tutti</b> i lettori di html.it
Questo perchè dalla versione 3.0 Rails effettua in automatico l'escape di tutte le stringhe che non siano state prima marcate come html_safe
, la corretta sintassi per giungere al risultato che ci aspettiamo diventa quindi:
<%= "Saluto <b>tutti</b> i lettori di html.it".html_safe %>
Chiaramente non ci troveremo spesso nella condizione di forzare una stringa ad html_safe
, questo perché tutti gli helper del framework incorporano già questa caratteristica.
Passiamo ora alla seconda funzionalità: se ricordate l'introduzione dovreste già aver associato ActiveModel a quell'oggetto che, se incorporato, garantisce il diritto di colloquio con ActionPack; un altro grande vantaggio di questa scelta architetturale è quello di poter includere all'interno di semplici class
Ruby metodi una volta patrimonio delle sole classi figlie di ActiveRecord::Base
.
Nel listato seguente ad esempio una classe Ruby acquista la possibilità di validare le proprie variabili di istanza semplicemente includendo il modulo ActiveModel::Validations
:
# file dog.rb class Dog # notate l'assenza di ActiveRecord::Base include ActiveModel::Validations # includo solo il componente delle validazioni validates_presence_of :age attr_accessor :age def initialize(age) @age = age end end
Conclusioni
Spero di essere riuscito in questo articolo a dare una panoramica dell'immensa quantità di nuove features e paradigmi implementati nella versione 3.0 di Ruby on Rails. Personalmente ritengo che questa release abbia tutte le carte in regola per ridefinire un nuovo standard qualitativo in termini di framework per applicazioni Web a patto che dimostri di aver mantenuto, nonostante il pesante refactoring, tutta la stabilità delle precedenti versioni.