#202 Active Record Queries in Rails 3
- Download:
- source codeProject Files in Zip (99.6 KB)
- mp4Full Size H.264 Video (15.1 MB)
- m4vSmaller H.264 Video (10.7 MB)
- webmFull Size VP8 Video (31.6 MB)
- ogvFull Size Theora Video (24 MB)
Negli ultimi due episodi vi abbiamo mostrato come configurare il vostro sistema per Rails 3 e come creare una nuova applicazione Rails 3. In questo episodio cominceremo a guardare ad alcune delle sue funzionalità, partendo con ActiveRecord che fornisce una nuova interfaccia per eseguire query verso il database. Pratik Naik di recente ha affrontato in dettaglio questo argomento in un post sul suo blog, che è consigliabile leggersi.
Alcuni semplici esempi
Per cominciare, vi mostreremo un po’ di esempi di chiamate find mediante ActiveRecord eseguite alla vecchia maniera e le convertiremo nel nuovo formato di Rails 3. Come pretesto, useremo una applicazione Rails basilare che definisce due modelli: Article
e Comment
in relazione fra loro - Article
has_many :comments
.
La prima find che aggiorniamo restituisce i dieci articoli pubblicati più di recente:
Article.find(:all, :order => "published_at desc", :limit => 10)
L’approccio di base per convertire una query ActiveRecord al nuovo formato di Rails 3 consiste nell’esaminare l’hash delle opzioni che sono passate alla find
e sostituirle con un metodo equivalente. Per cui, anzichè la find di sopra, useremo:
Article.order("published_at desc").limit(10)
Come potete vedere la nuova sintassi è facile da convertire dal formato precedente, ma ha una sintassi più pulita.
Non sempre, come si vede nel prossimo esempio, il vecchio hash di opzioni si mappa esattamente in nuovi metodi:
Article.find(:all, :conditions => ["published_at <= ?", Time.now], :include => :comments)
Ci sono solo due vere e proprie eccezioni alla regola generale di conversione e l’esempio qui sopra le comprende entrambe. La find qui sopra recupera tutti gli articoli che hanno una data di pubblicazione anteriore alla data corrente, insieme ai commenti associati. In Rails 3 tutto ciò diventa:
Article.where("published_at <= ?", Time.now).includes(:comments)
Anzichè :conditions
, ora usiamo il metodo where
, passandogli gli stessi argomenti che avremmo passato come valori della chiave :conditions
. Gli argomenti possono essere passati come array, ma è più chiaro passarli separatamente. Per ottenere anche i record associati, :include
è stata pluralizzata ed è divenuta il metodo includes
. Tutte le altre opzioni che avremmo normalmente passato alla find
diventano metodi con lo stesso nome che avevano le chiavi delle option precedenti.
Il nostro ultimo esempio recupera l’articolo pubblicato più di recente:
Article.find(:first, :order => "published_at desc")
Usando Rails 3, la sintassi diventa:
Article.order("published_at desc").first()
Si noti che non viene chiamata la first
se non alla fine della catena di metodi.
Dal momento che stiamo recuperando con un ordine descrescente, potremmo riscrivere la linea precedente come:
Article.order("published_at").last()
La query prodotta è la stessa, ma il codice è più conciso.
<<<<<<< HEADIn Rails 3.0 possiamo decidere di usare sia la vecchia sintassi per le find
, sia la nuova, ma da Rails 3.1 questa retrocompatibilit` comincierà ad essere deprecata e da Rails 3.2 sarà rimossa completamente. E’ bene dunque cominciare sin d’ora a portare le vecchie find alla nuova sintassi per far sì che le vostre applicazioni migrino bene a Rails 3 anche nelle sue future versioni.
In Rails 3.0 possiamo decidere di usare sia la vecchia sintassi per le find
, sia la nuova, ma da Rails 3.1 questa retrocompatibilità comincierà ad essere deprecata e da Rails 3.2 sarà rimossa completamente. E’ bene dunque cominciare sin d’ora a portare le vecchie find alla nuova sintassi per far sì che le vostre applicazioni migrino bene a Rails 3 anche nelle sue future versioni.
A questo punto potreste chiedervi qual è lo scopo della nuova sintassi, dal momento che prima o poi renderà non funzionanti molte applicazioni Rails oggi esistenti, una volta che per queste verrà aggiornato il framework. Ebbene, c’è una ragione per questo cambiamento e risiede nella potenza del lazy loading (caricamento posticipato - ndt).
Lazy Loading
Per spiegare il lazy loading, useremo la console della nostra applicazione. Se chiediamo al modello tutti gli articoli otteniamo un array come ci aspetteremmo, vedendo che ci sono tre articoli sul database.
ruby-1.9.1-p378 > Article.all => [#<Article id: 1, name: "It's Ancient", published_at: nil, hidden: false, created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 20:35:42">, #<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">, #<Article id: 3, name: "To the Future!", published_at: nil, hidden: false, created_at: "2010-02-22 20:38:17", updated_at: "2010-02-22 20:38:17">]
Se vogliamo avere tutti gli articoli in ordine alfabetico, possiamo usare il metodo order:
ruby-1.9.1-p378 > articles = Article.order("name") => #<ActiveRecord::Relation:0x00000101669b90 @table=#<Arel::Table:0x000001023e9af8 @name="articles", @options={:engine=>#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base, @adapter_name="SQLite">}, @engine=#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base, @adapter_name="SQLite">, …
Anzichè essere restituita una lista di articoli, questa volta ci viene dato un oggetto each
o prendessimo tutti o solo il primo degli articoli, allora, e solo allora, la query verrebbe eseguita effettivamente.
Anzichè essere restituita una lista di articoli, questa volta ci viene dato un oggetto ActiveRecord::Relation
. Questo oggetto contiene le informazioni sulla nostra find, ma fino ad ora la query al database non è ancora stata fatta. Questo è ciò che si intende per lazy loading in questo contesto: i dati non sono caricati dal database fintanto che non devono necessariamente esserlo (perchè utilizzati). Se ora scorressimo i record con una each
o prendessimo tutti o solo il primo degli articoli, allora, e solo allora, la query verrebbe eseguita effettivamente.
ruby-1.9.1-p378 > articles.first => #<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">
Ora vediamo come tutto ciò si applica alla nostra applicazione. Abbiamo generato uno scaffold per il modello dell’articolo in modo tale da avere un controller per gli articoli con le solite sette action. Il codice della action index
usa Article.all
per recuperare immediatamente tutti gli articoli:
/app/controllers/articles_controller.rb
def index @articles = Article.all respond_to do |format| format.html # index.html.erb format.xml { render :xml => @articles } end end
Se usassimo qualunque delle opzioni della find sugli articoli, come l’ordinamento per nome, verrebbe restituito un oggetto ActiveRecord::Relation
e la query non verrebbe eseguita sul controller. Verrebbe invece fatta nel codice della vista, nel momento in cui si comincia ad iterare col ciclo each Article
.
/app/views/articles/index.html.erb
<% @articles.each do |article| %> <tr> <td><%= article.name %></td> <td><%= article.published_at %></td> <td><%= article.hidden %></td> <td><%= link_to 'Show', article %></td> <td><%= link_to 'Edit', edit_article_path(article) %></td> <td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td> </tr> <% end %>
Se carichiamo la pagina index ora, gli articoli vengono mostrati in ordine alfabetico.
La cosa carina di tutto ciò è che se state usando il fragment caching mediante il metodo cache
nella vostra vista, ora funzionerà tutto meglio, dal momento che la query sul database non verrebbe eseguita se non necessario.
La nuova sintassi per le query rende più facile costruire condizioni di find. Diciamo che vogliamo filtrare gli articoli in modo tale che vengano mostrati solo quelli nascosti, se nella stringa di query nell’URL è stato specificato hidden=1
. Possiamo farlo cambiando la action index
in questo modo:
/app/controllers/articles_controller.rb
def index @articles = Article.order('name') <<<<<<< HEAD ======= >>>>>>> b97aa6197477c8dd0eb373f63b133400a451886f if params[:hidden] @articles = @articles.where(:hidden =>(params[:hidden] == "1")) end respond_to do |format| format.html # index.html.erb format.xml { render :xml => @articles } end end
Ora controlliamo se c’è un parametro hidden
fra quelli passati e se c’è aggiungiamo il metodo where
in coda alla find, che filtrerà sui soli articoli nascosti se tale parametro varrà 1
. Se aggiungiamo quel parametro all’URL e ricarichiamo la pagina, vediamo effettivamente solo gli articoli nascosti:
Analogamente, se passiamo 0
come valore del parametro, vediamo solo gli articoli visibili.
Essere in grado di concatenare fra loro metodi come questi è apre la strada alla possibilità di poter costruire query molto più complesse, poichè sappiamo che la query non sarà eseguita effettivamente fino a che i dati non saranno necessari.
Named Scope
Ora vi mostriamo alcuni cambiamenti sui named scopes introdotti da Rails 3. Sotto c’è la nostra classe di modello Article
con due named scope, uno per recuperare gli articoli visibili e uno per recuperare gli articoli che sono stati pubblicati:
/app/models/article.rb
class Article < ActiveRecord::Base named_scope :visible, :conditions => ["hidden != ?", true] named_scope :published, lambda { {:conditions => ["published_at <= ?", Time.zone.now]} } end
Questi named scope sono definiti alla maniera di Rails 2, ma in Rails 3 l’approccio è leggermente differente. La prima differenza è che il metodo per definire un named scope non è più named_scope
ma solo scope
. Poi non si passano più le condizioni come hash, ma si usano, come per la find, dei metodi. Come abbiamo fatto con i nuovi metodi find, usiamo la where
anzichè :conditions
. Nella sintassi Rails 3 i named scope appariranno dunque così:
/app/models/article.rb
class Article < ActiveRecord::Base scope :visible, where("hidden != ?", true) scope :published, lambda { where("published_at <= ?", Time.zone.now) } end
Un’altra nuova funzionalità è la possibilità di assemblare fra loro gli scope. Se vogliamo creare uno scope chiamato recent
che restituisce gli articoli visibili recentemente pubblicati ordianti per data di pubblicazione, possiamo farlo riutilizzando i due scope che già abbiamo:
scope :recent, visible.published.order("published_at desc")
Tutto ciò che abbiamo fatto nel nostro nuovo scope è stato di concatenare insieme i due scope che già avevamo e aggiungere un metodo order
.
Possiamo subito provare il nostro nuovo scope così fatto nella console. Se invochiamo Article.recent
viene restituito un oggetto ActiveRecord::NamedScope::Scope
. Questo oggetto si comporta in modo simile all’oggetto ActiveRecord::Relation
visto prima:
ruby-1.9.1-p378 > Article.recent => #<ActiveRecord::NamedScope::Scope:0x0000010318bd08 @table=#<Arel::Table:0x00000102740ea8 @name="articles", @options={:engine=>#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>}, @engine=#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>>, …
Se chiamiamo all
sull’oggetto Scope
, vedremo restituiti gli articoli che fanno match:
ruby-1.9.1-p378 > Article.recent.all => [#<Article id: 1, name: "It's Ancient", published_at: "2010-01-01", hidden: false, created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 23:00:16">]
Suggerimento finale
Chiudiamo questo episodio con un utile suggerimento. Se avete un oggetto Relation
o Scope
e volete vedere l’SQL che verrebbe prodotto al lancio effettivo, potete chiamare sull’oggetto il metodo to_sql
:
ruby-1.9.1-p378 > Article.recent.to_sql => "SELECT \"articles\".* FROM \"articles\" WHERE (hidden != 't') AND (published_at <= '2010-02-22 22:47:12.023289') ORDER BY published_at desc"
Questo metodo mostra l’SQL che ActiveRecord inoltrerà al database per ottenere gli articoli pubblicati di recente e non nascosti.
con questo si chiude l’episodio sull’utilizzo delle query ActiveRecord in Rails 3. Ci sono molte altre interessanti aggiunte che renderanno il codice dei vostri controller Rails molto più conciso ed espressivo. Tutto il tempo speso a giocare con queste nuove feature sarà presto ripagato.