Le applicazioni Web moderne spesso si trovano nella condizione di dover rispondere sia alle richieste che pervengono dai browser degli utenti (quindi nella maggior parte dei casi attraverso l'utilizzo di HTML), sia a quelle che arrivano da altre applicazioni Web.
Alcune tecniche utilizzate per gestire quest'ultima casistica riguardano l'utilizzo di protocolli come SOAP o XML-RPC che, pur essendo valide soluzioni al problema comportano il più delle volte la necessità di stendere una struttura applicativa dedicata a questi scenari.
Rails introduce in questo frangente una implementazione innovativa, mixando sapientemente il paradigma REST e la tecnologia XML per dar vita ad uno tra i più potenti mezzi di cui questo framework ci omaggia: ActiveResource.
La magia del "respond_to"
Chiunque abbia mai sperimentato l'utilizzo del generatore scaffold
non potrà non aver notato la porzione di codice che conclude ogni azione del controller creato. Per fare un esempio creiamo lo scaffold per un'ipotetica entita project
:
ruby script/generate scaffold project name:string duedate:date description:text
dirigiamoci quindi verso il file projects_controller.rb
ed esaminiamolo con un qualsiasi editor testuale, noteremo il seguente blocco di codice:
respond_to do |format| format.html # index.html.erb format.xml { render :xml => @projects } end
Queste poche righe racchiudono una funzionalità molto potente di Rails: la possibilità di variare il tipo di formattazione dell'informazione generata utilizzando come discriminante il formato (termine generico che delineerò nel prossimo paragrafo) con cui è stata formulata la richiesta.
In altre parole se cerchermo di accedere a http://localhost:3000/projects
otterremo la canonica pagina che contraddistingue ogni indice di scaffold; ma se punteremo il nostro browser all'indirizzo http://localhost:3000/projects.xml
il server ci risponderà con lo stesso contenuto informativo precedente, questa volta però formattato in XML.
ActiveResource utilizza l'XML prodotto in questo modo per consentirci di navigare all'interno di un model che non appartiene alla nostra applicazione; sfruttando sapientemente le convenzioni che sottendono url e verbi di un tipico controller RESTful esso può infatti darci le stesse possibilità che sperimentiamo giornalmente con ActiveRecord.
ActiveResource in pratica
ActiveResource, come ActiveRecord, agisce attraverso i models
(nello specifico chiamati resources
). Come sappiamo questi sono tutte quelle classi che si preoccupano del mantenimento dello stato della nostra applicazione. I models che ereditano da ActiveRecord::Base
utilizzano un database per leggere e scrivere le informazioni di cui sono custodi; viceversa i models
che utilizzano ActiveResource
(e quindi ereditano da ActiveResource::Base
) utilizzano un controller di un'altra applicazione come "base dati" presso la quale recuperare e salvare informazioni (sfruttando il formato XML).
Questa descrizione può sembrare, ad una prima lettura, poco esplicativa ed è quindi bene supportarla con esempi pratici: supponiamo di dover creare una nuova applicazione che si colleghi a quella delineata nella prima parte dell'articolo per effettuare alcune operazioni sui progetti in essa contenuti.
Per prima cosa creiamo la nuova applicazione:
rails ar_demo
poi creiamo un nuovo file chiamato project.rb
all'interno della directory app/models
della nostra applicazione, apriamolo con un editor di testo e inseriamo il seguente codice:
class Project < ActiveResource::Base self.site = "http://localhost:3000" end
L'unica dichiarazione all'interno della classe serve per specificare a che indirizzo questo modello dovrà connettersi per recuperare le informazioni richieste.
A questo punto possiamo testare quanto appena creato; eseguiamo il server dell'applicazione creata nel primo capitolo (quella con lo scaffold) e lanciamo successivamente la console di ar_demo
.
ruby script/console
All'interno della console possiamo sperimentare (e scoprire) come la risorsa Project
sia molto simile (almeno nei metodi) alla sua controparte di ActiveRecord; infatti possiamo eseguire:
>> Project.find(:all) => []
Al momento la query ritorna un array vuoto in quanto non abbiamo ancora inserito alcun progetto, per farlo possiamo utilizzare l'interfaccia messaci a disposizione dallo scaffold nella prima applicazione, oppure generare attraverso ActiveResource un nuovo oggetto della classe Project
, proprio come se stessimo lavorando su un comunissimo modello:
Esempio di output su console
>> p1 =Project.new(:name=>"demo",:duedate=>Time.now,:description=>"progetto di demo") => #<Project:0x105b760 @prefix_options={}, @attributes={"name"=>"demo", "duedate"=>Wed Jun 11 00:10:41 +0000 2008, "description"=>"progetto di demo"}> >> p1.save => true
A questo punto le potenzialità di questo strumento cominciano a trasparire nella loro interezza; quello che abbiamo fatto con queste poche righe è stato pilotare un'entità appartenente ad una applicazione remota, attraverso un'istanza locale di ActiveResource. Abbiamo eseguito operazioni CRUD che sono state dinamicamente tradotte in costrutti RESTful e mandate all'applicazione remota, la quale, dopo averle eseguite, ha restituito un responso in XML utilizzando il blocco respond_to
. Tale responso è stato infine correttamente re-interpretato dall'istanza locale, come fosse giunto da un'interrogazione al database.
Se ora proviamo ad eseguire nuovamente il metodo find otteremo come risposta il record appena creato:
>> Project.find(:all) => [#<Project:0x2737ec0 @prefix_options={}, @attributes={"name"=>"demo", "updated_at"=>Wed Jun 11 00:10:43 UTC 2008, "id"=>1, "duedate"=>Wed, 11 Jun 2008, "description"=>"progetto di demo", "created_at"=>Wed Jun 11 00:10:43 UTC 2008}>]
ActiveResource come AcriveRecord
Com'è possibile intuire ActiveResource ripropone i più importanti metodi di ActiveRecord in chiave RESTful, consentendoci non solo di creare e ricercare elementi, ma anche di effettuare modifiche ed eliminazioni con la stessa sintassi usata nei normali models:
>> p = Project.find(:first) => #<Project:0x26a10d8 @prefix_options={}, @attributes={"name"=>"demo", "updated_at"=>Wed Jun 11 00:10:43 UTC 2008, "id"=>1, "duedate"=>Wed, 11 Jun 2008, "description"=>"progetto di demo", "created_at"=>Wed Jun 11 00:10:43 UTC 2008}> >> p.name="Progetto demo" => "Progetto demo" >> p.save => true >> p.destroy => #<Net::HTTPOK 200 OK readbody=true>
Ma le potenzialità di questo package non si limitano a questo: anche le associazioni fra diverse entità remote possono essere gestite in modo trasparente da ActiveResource!
Per fare un esempio supponiamo di aver creato un nuovo model chiamato items
nella prima applicazione e di aver stabilito una relazione 1 a n
tra projects
e items
(ogni projects
ha molti items
). Supponiamo infine di aver creato un controller RESTful per gestire le operazioni CRUD sugli items
in modo da poter editare e modificare gli items di ogni progetto. Nel file config/routes.rb
dovremmo quindi trovarci con una situazione simile a:
map.resources :projects, :has_many => [:items ]
In questo contesto, se tutto è stato fatto a dovere una chiamata all'indirizzo:
/projects/5/items
dovrebbe restiturci l'elenco degli items relativi al progetto con id=5
.
La struttura RESTful 1 a n
qui velocemente riassunta, se implementata correttamente, fa si che la seconda applicazione possa beneficare dell'associazione tra items e progetti.
Per provare quanto appena enunciato creiamo una seconda resource
all'interno della seconda applicazione (file: models/items.rb
):
class Item < ActiveResource::Base self.site = "http://localhost:3000/projects/:project_id" end
Eseguiamo quindi la console e, mentre la prima appliazione è in esecuzione, proviamo ad effettuare delle ricerche:
>> Item.find(:all, :params => { :project_id => 1 }) => [#<Item: ... }>]
Allo stesso modo possiamo eseguire anche inserimenti, modifiche e cancellazioni.
Conclusioni
ActiveResource può, in sintesi, essere considerato come la controparte RESTful di ActiveRecord, i due package insistono su mezzi diversi ma ci consentono entrambi (circa) le stesse funzionalità. Tutte le dimostrazioni effettuate in questo articolo possono essere testate utilizzando le due applicazioni di esempio allegate; app1
contiene i due scaffold (projects e items) mentre app2
contiene le due resources con le quali effettuare le prove attraverso la console.