#215 Advanced Queries in Rails 3
- Download:
- source codeProject Files in Zip (105 KB)
- mp4Full Size H.264 Video (17.5 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (30.1 MB)
- ogvFull Size Theora Video (27.6 MB)
Questo episodio tratterà le query avanzate di Rails 3. Nell’episodio 202 [guardalo, leggilo] abbiamo parlato delle novità alle query di ActiveRecord di Rails 3; oggi proseguiremo il discorso da lì e mostreremo alcune altre feature avanzate.
Utilizzo dei metodi di classe al posto degli scope
L’applicazione che useremo ha due modelli: Product
e Category
con il prodotto che appartiene ad una categoria. Il modello prodotto ha due named scope: discontinued
, che restituisce tutti i prodotti per i quali il valore del campo discontinued è true e price, che restituisce i prodotti più economici del prezzo passato come argomento:
class Product < ActiveRecord::Base belongs_to :category scope :discontinued, where(:discontinued => true) scope :cheaper_than, lambda { |price| where("price < ?", price) } end
Nel secondo named scope si usa un lambda. Se non ne avete mai usato uno di questi in un named scope, potreste valutare piuttosto l’uso di un metodo di classe, specialmente se state passando un consistente numero di parametri o se il contenuto dello scope è complesso. Il nostro invece è piuttosto semplice, ma lo cambiamo ugualmente con un metodo di classe:
class Product < ActiveRecord::Base belongs_to :category scope :discontinued, where(:discontinued => true) def self.cheaper_than(price) where("price < ?", price) end end
Il metodo di classe si comporterà alla stessa maniera dello scope. Possiamo fare la stessa cosa anche in Rails 2, ma si comporta molto meglio in Rails 3. Possiamo perfino riusare questo metodo in un altro named scope. Se vogliamo creare uno scope chiamato cheap
che restituisce i prodotti che costano meno di cinque euro, possiamo scrivere:
scope :cheap, cheaper_than(5)
Tuttavia potenzialmente ci può essere un’insidia in tutto ciò. Se usiamo un metodo di classe in uno scope, dobbiamo definire tale scope, all’interno della classe, dopo che è stato definito il metodo di classe, il che significa che lo scope dovrà essere messo più in basso nella classe rispetto al solito.
Usando la console Rails, possiamo vedere l’SQL generato dallo scope:
ruby-1.8.7-p249 > Product.cheap.to_sql => "SELECT \"products\".* FROM \"products\" WHERE (price < 5)"
Se chiamiamo semplicemente lo scope, vedremo la lista dei prodotti più economici:
>Product.cheap => [#<Product id: 1, name: "Top", price: 4.99, discontinued: nil, category_id: 3, created_at: "2010-05-24 21:01:59", updated_at: "2010-05-24 21:01:59">, #<Product id: 2, name: "Milk", price: 2.99, discontinued: nil, category_id: 2, created_at: "2010-05-24 21:02:38", updated_at: "2010-05-24 21:02:38">]
Associazioni
Finchè siamo in console, vi mostriamo un altro trucco, che coinvolge l’utilizzo degli scope mediante le associazioni. Come abbiamo citato in precedenza, nella nostra applicazione un prodotto appartiene ad una categoria. Ciò significa che possiamo usare il metodo joins
per ottenere una query SQL che esegua una inner join fra le tabelle prodotti e categorie:
ruby-1.8.7-p249 > Category.joins(:products).to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\""
Vogliamo trovare tutte le categorie che hanno un prodotto che faccia match con un certo scope; tanto per fissare le idee, vogliamo trovare tutte le categorie che hanno almeno un prodotto che costi meno di cinque euro. Ci sono due metodi che possiamo usare a questo scopo. Possiamo usare il metodo merge
in questa maniera:
> Category.joins(:products).merge(Product.cheap) => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
Oppure in alternativa possiamo usare una e commerciale che è un alias alla stessa cosa, per cui restituirà lo stesso risultato:
> Category.joins(:products) & Product.cheap => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
Usando questi metodi, possiamo mettere in join qualsiasi altra query, anche se non si trova sullo stesso modello, consentendoci di trovare tutte le categorie che hanno dei prodotti che costano meno di cinque euro. Se invochiamo la to_sql
sulle query in join vedremo che è stata usata la inner join e che la clausola di WHERE del named scope è stata aggiunta in fondo all’istruzione:
> (Category.joins(:products) & Product.cheap).to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\" WHERE (price < 5)"
Questa può rivelarsi una tecnica potente, allorchè usata all’interno della definizione di un named scope. La possiamo usare, per esempio, per creare uno scope nel modello Category
, che trovi le categorie aventi prodotti economici:
class Category < ActiveRecord::Base has_many :products scope :with_cheap_products, joins(:products) & Product.cheap end
Questo nuovo named scope restituirà le stesse categorie della query in join che abbiamo scritto poco fa:
> Category.with_cheap_products => [#<Category id: 3, name: "Games", created_at: "2010-05-24 21:00:57", updated_at: "2010-05-25 18:30:18">, #<Category id: 2, name: "Groceries", created_at: "2010-05-24 21:00:50", updated_at: "2010-05-25 18:30:39">]
Una cosa di cui stare attenti quando si tratta con condizioni fra associazioni è il nome delle tabelle. Se diamo un’occhiata all’SQL prodotto alla chiamata del named scope, possiamo vedere che non c’è alcun nome di tabella che qualifichi il campo nella clausola di WHERE:
> Category.with_cheap_products.to_sql => "SELECT \"categories\".* FROM \"categories\" INNER JOIN \"products\" ON \"products\".\"category_id\" = \"categories\".\"id\" WHERE (price < 5)"
Questa cosa sarebbe un problema se entrambe le tabelle avessero una colonna chiamata price, perchè si creerebbe un’ambiguità sul nome della colonna nella query SQL. Si può superare questa ambiguità definendo esplicitamente il nome della tabella nel modello Product
:
def self.cheaper_than(price) where("products.price < ?", price) end
Ora non c’è più alcun pericolo di ambiguità sul nome della colonna. Dovreste sempre specificare il nome della tabella quando usate stringhe SQL nelle condizioni di find. Comunque, se state usando un hash, come negli scope, non c’è bisogno di preoccuparsi del nome della tabella, dal momento che Rails lo aggiunge in automatico per noi.
Costruire record mediante named scope
La prossima cosa che vi mostriamo è il modo di costruire record usando i named scope. Abbiamo un named scope definito nella nostra classe di modello Product
, chiamato discontinued, che trova tutti i prodotti dismessi:
> Product.discontinued => [#<Product id: 3, name: "Some DVD", price: 13.49, discontinued: true, category_id: 1, created_at: "2010-05-25 19:45:05", updated_at: "2010-05-25 19:45:05">]
Poichè il named scope usa un hash, possiamo chiamare il metodo build
questo per costruire un nuovo prodotto con l’attributo discontinued
già impostato a true:
> p = Product.discontinued.build => #<Product id: nil, name: nil, price: nil, discontinued: true, category_id: nil, created_at: nil, updated_at: nil>
Il nuovo prodotto così creato risulterà già dismesso in quanto questo attributo fa parte della condizione di where
. Tutto ciò funziona in maniera simile ad una associazione, in quanto quando si chiama il metodo build sull’associazione, automaticamente viene impostata la foreign key associata a quel record. In questo caso, tuttavia, stiamo più semplicemente assegnando il valore agli attributi che sono inclusi nella condizione di where
. Questa è una cosa utile da sapere se si ha la necessità di creare dei record che rispettino un determinato named scope.
Arel
Concludiamo questo episodio presentando Arel. Arel gestisce le query ActiveRecord dietro le quinte. Probabilmente non avrete molto spesso il bisogno di interfacciarvi direttamente con questo livello, ma è utile capire come il tutto funzioni e comprendere ciò di cui questo framework è capace, nel caso in cui ci sia mai il bisogno di doverlo usare direttamente.
Per accedere direttamente ad Arel in Rails 3, si può richiedere al modello una arel_table
, per cui prendiamo l’arel_table
per il modello Product dalla console e assegnamola ad una variabile:
> t = Product.arel_table
Questo oggetto è una rappresentazione della tabella dei prodotti. Possiamo avere accesso alle colonne nella tabella in questo modo:
>t[:price] => <Attribute price>
Possiamo chiamare metodi sugli attributi per eseguire delle condizioni di find. Per esempio, se vogliamo trovare tutti i prodotti che costano 2.99 euro, possiamo lanciare:
> t[:price].eq(2.99) => #<Arel::Predicates::Equality:0x1040dd9f0 @operand1=<Attribute price>, @operand2=2.99>
Questa istruzione restituisce un predicato che fondamentalmente è la condizione di find. Ci sono anche altri metodi che possiamo chiamare, come il metodo matches
, che esegue una query di tipo LIKE. Come per le altre query, possiamo invocare il to_sql
per vedere l’effettivo codice SQL prodotto dalla query.
> t[:name].matches('%lore').to_sql => "\"products\".\"name\" LIKE '%lore'"
Possiamo usare il metodo or
per concatenare i predicati, per cui, per esempio, possiamo cercare i prodotti che costano 2.99 euro o che hanno un nome che termina per ‘lore’:
t[:price].eq(2.99).or(t[:name].matches('%lore'))
Questa istruzione restituirà una combinazione dei due predicati: possiamo vedere l’SQL prodotto chiamando to_sql
come abbiamo fatto anche prima:
> t[:price].eq(2.99).or(t[:name].matches('%lore')).to_sql => "(\"products\".\"price\" = 2.99 OR \"products\".\"name\" LIKE '%lore')"
Possiamo passare i predicati come argomenti al metodo where
di un oggetto ActiveRecord. Ciò significa che possiamo passare il predicato che abbiamo creato alla chiamata Product.where
al fine di restituire tutti i prodotti il cui prezzo sia 2.99 euro, o il cui nome termini per ‘lore’:
> Product.where(t[:price].eq(2.99).or(t[:name].matches('%lore'))) => [#<Product id: 2, name: "Milk", price: 2.99, discontinued: nil, category_id: 2, created_at: "2010-05-24 21:02:38", updated_at: "2010-05-24 21:02:38">, #<Product id: 4, name: "Knight Lore", price: 2.99, discontinued: nil, category_id: nil, created_at: "2010-05-26 19:36:02", updated_at: "2010-05-26 19:36:02">]
Abbiamo solo dato uno sguardo rapido ad Arel in queste poche righe; Arel è in grado di fare molte più cose e ActiveRecord espone solo una piccola parte di queste. Ci sono una serie di plugin che sfruttano tutta la potenza di Arel e la rendono disonibile in una interfaccia più semplice, come ad esempio MetaWhere. Questo plugin da accesso ai metodi di Arel come il matches
e l’eq
dall’interno dell’hash delle condizioni, in questo modo:
Product.where(:price.eq => 2.99, :name.matches => '%lore')
Questo vi da molta più flessibilità sulla definizione dell’hash delle condizioni e vale la pena darci un’occhiata se avete la necessità di eseguire query più complesse sui vostri modelli.