Queries in Rails, parte 2

In questo secondo articolo, ci addentreremo un po 'più in profondità nelle query di Active Record in Rails. Nel caso in cui tu sia ancora nuovo in SQL, aggiungerò degli esempi che sono abbastanza semplici da poter essere etichettati e raccogliere la sintassi un po 'mentre andiamo. 

Detto questo, sarebbe sicuramente di aiuto se si esegue un rapido tutorial SQL prima di tornare per continuare a leggere. Altrimenti, prenditi il ​​tuo tempo per capire le query SQL che abbiamo usato, e spero che entro la fine di questa serie non si sentirà più intimidire. 

La maggior parte è davvero semplice, ma la sintassi è un po 'strana se si inizia con la programmazione, specialmente in Ruby. Aspetta lì, non è una scienza missilistica!

Temi

  • Include & Eager Loading
  • Partecipare alle tabelle
  • Carico
  • Scopes
  • aggregazioni
  • Dynamic Finders
  • Campi specifici
  • SQL personalizzato

Include & Eager Loading

Queste query includono più di una tabella di database con cui lavorare e potrebbero essere le più importanti da rimuovere da questo articolo. Si riduce a questo: invece di fare più query per informazioni che sono distribuite su più tabelle, include cerca di mantenere questi al minimo. Il concetto chiave alla base di questo è chiamato "caricamento ansioso" e significa che stiamo caricando oggetti associati quando facciamo una ricerca.

Se lo facessimo ripetendo su una collezione di oggetti e poi cercando di accedere ai suoi record associati da un'altra tabella, avremmo incontrato un problema chiamato "N + 1 query problem". Ad esempio, per ciascuno agent.handler in una raccolta di agenti, dovremmo generare query separate per entrambi gli agenti e i loro gestori. Questo è ciò che dobbiamo evitare dal momento che questo non si adatta affatto. Invece, facciamo quanto segue:

Rails

agenti = Agente.include (: gestori)

Se ora iteriamo su una tale serie di agenti-scontando che non abbiamo limitato il numero di record restituiti per ora-finiremo con due query invece che forse un gazillion. 

SQL

SELEZIONA "agenti". * DA "agenti" Seleziona "gestori". * DA "gestori" DOVE "gestori". "Id" IN (1, 2)

Questo agente nella lista ha due gestori e quando ora chiediamo all'agente oggetti per i suoi gestori, non è necessario licenziare query di database aggiuntive. Possiamo fare un ulteriore passo avanti, naturalmente, e desideriamo caricare più record di tabella associati. Se avessimo bisogno di caricare non solo i gestori, ma anche le missioni associate dell'agente per qualsiasi motivo, potremmo usare include come questo.

Rails

agenti = Agente.include (: gestori,: missione)

Semplice! Basta fare attenzione sull'utilizzo di versioni singolari e plurali per gli include. Dipendono dalle tue associazioni di modelli. UN ha molti l'associazione usa il plurale, mentre a appartiene a o a Ha uno ha bisogno della versione singolare, ovviamente. Se hai bisogno, puoi anche rimboccarti un dove clausola per specificare condizioni aggiuntive, ma il modo preferito di specificare le condizioni per le tabelle associate che sono caricate con interesse è usando si unisce anziché. 

Una cosa da tenere a mente sul caricamento ansioso è che i dati che verranno aggiunti verranno inviati per intero a Active Record, che a sua volta costruisce oggetti Ruby compresi questi attributi. Questo è in contrasto con "semplicemente" unendo i dati, dove si otterrà un risultato virtuale che è possibile utilizzare per i calcoli, ad esempio, e sarà meno drenante della memoria di.

Partecipare alle tabelle

Unire le tabelle è un altro strumento che consente di evitare l'invio di troppe query non necessarie lungo la pipeline. Uno scenario comune è l'unione di due tabelle con una singola query che restituisce una sorta di record combinato. si unisce è solo un altro metodo di ricerca di Active Record che ti consente di usare termini SQL-ADERIRE tabelle. Queste query possono restituire i record combinati da più tabelle e si ottiene una tabella virtuale che combina i record di queste tabelle. Questo è piuttosto interessante quando si confronta quello per sparare tutti i tipi di query per ogni tabella. Esistono diversi tipi di sovrapposizione di dati che è possibile ottenere con questo approccio. 

L'inner join è il modus operandi predefinito per si unisce. Corrisponde a tutti i risultati che corrispondono a un determinato ID e alla sua rappresentazione come chiave esterna da un altro oggetto o tabella. Nell'esempio qui sotto, metti semplicemente: dammi tutte le missioni in cui è la missione id si presenta come mission_id nel tavolo di un agente. "agenti". "mission_id" = "missioni". "id". I join interni escludono relazioni che non esistono.

Rails

Mission.joins (: Agenti)

SQL

SELEZIONA "missioni". * FROM "missions" INNER JOIN "agenti" ON "agenti". "Mission_id" = "mission". "Id"

Quindi stiamo abbinando le missioni e i loro agenti di accompagnamento in un'unica query! Certo, potremmo ottenere le missioni prima, scorrere su di esse una ad una e chiedere i loro agenti. Ma poi torneremmo al nostro terribile "problema di query N + 1". No grazie! 

Ciò che è anche bello di questo approccio è che non otterremo nessun caso zero con join interni; riceviamo solo i record restituiti che corrispondono ai loro id a chiavi esterne nelle tabelle associate. Se abbiamo bisogno di trovare missioni, ad esempio, che mancano agenti, avremmo invece bisogno di un join esterno. Dal momento che questo attualmente comporta la scrittura del proprio ESTERNO ORA SQL, esamineremo questo nell'ultimo articolo. Torna ai join standard, ovviamente puoi anche unirti a più tabelle associate.

Rails

Mission.joins (: agenti,: spese,: gestori)

E puoi aggiungerne alcuni dove clausole per specificare ancora di più i tuoi cercatori. Sotto, stiamo cercando solo le missioni eseguite da James Bond e solo gli agenti che appartengono alla missione 'Moonraker' nel secondo esempio.

Mission.joins (: agenti) .where (agenti: nome: 'James Bond')

SQL

SELEZIONA "missioni". * DA "missioni" INNER JOIN "agenti" ON "agenti". "Mission_id" = "missioni". "Id" WHERE "agenti". "Nome" =? [["nome", "James Bond"]]

Rails

Agent.joins (: mission) .where (missions: mission_name: 'Moonraker')

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Mission_name" =? [["mission_name", "Moonraker"]]

Con si unisce, devi anche prestare attenzione all'uso singolare e plurale delle associazioni di modelli. Perché il mio Missione classe has_many: agenti, possiamo usare il plurale. D'altra parte, per il Agente classe belongs_to: missione, solo la versione singolare funziona senza far esplodere. Importante piccolo dettaglio: il dove parte è più semplice. Dal momento che si sta eseguendo la scansione di più righe nella tabella che soddisfano una determinata condizione, la forma plurale ha sempre senso.

Scopes

Gli ambiti sono un modo pratico per estrarre le esigenze di query comuni in metodi ben definiti. In questo modo sono un po 'più facili da aggirare e anche più facilmente capire se altri devono lavorare con il tuo codice o se hai bisogno di rivedere alcune query in futuro. È possibile definirli per singoli modelli ma utilizzarli anche per le loro associazioni. 

Il cielo è davvero il limite-si unisce, include, e dove sono tutti un gioco leale! Anche gli ambiti tornano ActiveRecord :: Relazioni oggetti, puoi incatenarli e chiamare altri ambiti sopra di loro senza esitazione. Estrarre ambiti del genere e incatenarli a query più complesse è molto utile e rende quelli più lunghi sempre più leggibili. Gli ambiti sono definiti tramite la sintassi "stabby lambda":

Rails

classe missione < ActiveRecord::Base has_many: agents scope :successful, -> where (mission_complete: true) end Mission.successful
agente di classe < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) scope: gambler, -> where (gambler: true) end # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

SELEZIONA "agenti". * DA "agenti" DOVE "agenti". "Licence_to_kill" =? E "agenti". "Donnaiolo" =? E "agenti". "Giocatore d'azzardo" =? [["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Come puoi vedere dall'esempio sopra, trovare James Bond è molto più bello quando puoi mettere insieme gli ambiti. In questo modo puoi mescolare e abbinare le varie query e rimanere DRY allo stesso tempo. Se hai bisogno di obiettivi tramite associazioni, sono a tua disposizione:

Mission.last.agents.licenced_to_kill.womanizer.gambler
SELEZIONA "missioni". * FROM "missions" ORDER BY "missions". "Id" DESC LIMIT 1 SELECT "agents". * FROM "agents" WHERE "agents". "Mission_id" =? AND "agenti". "Licence_to_kill" =? E "agenti". "Donnaiolo" =? E "agenti". "Giocatore d'azzardo" =? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Puoi anche ridefinire il default_scope per quando stai guardando qualcosa di simile Mission.all.

classe missione < ActiveRecord::Base default_scope  where status: "In progress"  end Mission.all

SQL

 SELEZIONA "missioni". * FROM "missions" WHERE "missions". "Status" =? [["stato", "In corso"]]

aggregazioni

Questa sezione non è tanto avanzata in termini di comprensione, ma ne avrai bisogno il più delle volte in scenari che possono essere considerati un po 'più avanzati del tuo cercatore medio. .tutti, .primo, .find_by_id o qualsiasi altra cosa Il filtraggio basato su calcoli di base, ad esempio, è più probabile che i neofiti non entrino in contatto subito. Cosa stiamo guardando esattamente qui?

  • somma
  • contare
  • minimo
  • massimo
  • media

Peasy facile, giusto? La cosa interessante è che invece di eseguire il ciclo di una raccolta di oggetti restituita per eseguire questi calcoli, possiamo lasciare che Active Record esegua tutto questo per noi e restituisca questi risultati con le query, preferibilmente in una query. Bello, eh?

  • contare

Rails

Mission.count # => 24

SQL

SELEZIONA CONTO (*) DA "missioni"
  • media

Rails

Agent.average (: number_of_gadgets) .to_f # => 3.5

SQL

SELECT AVG ("agent". "Number_of_gadgets") DA "agenti"

Poiché ora sappiamo come possiamo fare uso di si unisce, possiamo fare un ulteriore passo avanti e chiedere solo la media dei gadget che gli agenti hanno su una particolare missione, per esempio.

Rails

Agent.joins (: mission) .where (missions: name: 'Moonraker'). Average (: number_of_gadgets) .to_f # => 3.4

SQL

SELECT AVG ("agent". "Number_of_gadgets") DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Name" =? [["nome", "Moonraker"]]

Raggruppare questi numeri medi di gadget per i nomi delle missioni diventa banale a quel punto. Vedi di più sul raggruppamento qui sotto:

Rails

Agent.joins (: missione) .group ( 'missions.name') media. (: Number_of_gadgets)

SQL

SELECT AVG ("agent". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" GROUP BY missions.name
  • somma

Rails

Agent.sum (: number_of_gadgets) Agent.where (licence_to_kill: true) .sum (: number_of_gadgets) Agent.where.not (licence_to_kill: true) .sum (: number_of_gadgets)

SQL

SELECT SUM ("agent". "Number_of_gadgets") FROM "agents" SELECT SUM ("agents". "Number_of_gadgets") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]] SELECT SUM ("agent". "number_of_gadgets") FROM "agents" WHERE ("agents". "licence_to_kill"! =?) [["licence_to_kill", "t"]]
  • massimo

Rails

Agent.maximum (: number_of_gadgets) Agent.where (licence_to_kill: true) .maximum (: number_of_gadgets) 

SQL

SELECT MAX ("agent". "Number_of_gadgets") FROM "agents" SELECT MAX ("agent". "Number_of_gadgets") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]
  • minimo

Rails

Agent.minimum (: iq) Agent.where (licence_to_kill: true) .minimum (: iq) 

SQL

SELECT MIN ("agents". "Iq") FROM "agents" SELECT MIN ("agents". "Iq") FROM "agents" WHERE "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]

Attenzione!

Tutti questi metodi di aggregazione non ti consentono di collegarti ad altre cose: sono terminali. L'ordine è importante per fare calcoli. Non otteniamo un ActiveRecord :: Relation oggetto di ritorno da queste operazioni, che interrompe la musica in quel punto, otteniamo invece un hash o numeri. Gli esempi seguenti non funzioneranno:

Rails

Agent.maximum (: number_of_gadgets) .where (licence_to_kill: true) Agent.sum (: number_of_gadgets) .where.not (licence_to_kill: true) Agent.joins (: mission) .average (: number_of_gadgets) .group ('missions.name ')

raggruppate

Se si desidera che i calcoli vengano suddivisi e ordinati in gruppi logici, è necessario utilizzare a GRUPPO clausola e non farlo in Ruby. Ciò che intendo è che dovresti evitare di scorrere su un gruppo che produce potenzialmente tonnellate di domande.

Rails

Agent.joins (: mission) .group ('missions.name'). Average (: number_of_gadgets) # => "Moonraker" => 4.4, "Octopussy" => 4.9

SQL

SELECT AVG ("agent". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" GROUP BY missions.name

Questo esempio trova tutti gli agenti raggruppati in una determinata missione e restituisce un hash con il numero medio calcolato di gadget come valori, in una singola query! Sì! Lo stesso vale per gli altri calcoli, ovviamente. In questo caso, ha davvero più senso lasciare che SQL faccia il lavoro. Il numero di query che spariamo per queste aggregazioni è semplicemente troppo importante.

Dynamic Finders

Per ogni attributo sui tuoi modelli, ad esempio nome, indirizzo emailfavorite_gadget e così via, Active Record ti consente di utilizzare metodi di ricerca molto leggibili creati dinamicamente per te. Sembra criptico, lo so, ma non significa altro che find_by_id o find_by_favorite_gadget. Il find_by la parte è standard e Active Record si limita a posizionare il nome dell'attributo per te. Puoi persino aggiungere un ! se vuoi che il cercatore rilasci un errore se non si trova nulla. La parte malata è che puoi anche concatenare questi metodi di ricerca dinamica. Proprio come questo:

Rails

Agent.find_by_name ('James Bond') Agent.find_by_name_and_licence_to_kill ('James Bond', vero)

SQL

SELEZIONA "agenti". * DA "agenti" DOVE "agenti". "Nome" =? LIMIT 1 [["name", "James Bond"]] SELEZIONA "agenti". * DA "agenti" DOVE "agenti". "Nome" =? AND "agenti". "Licence_to_kill" =? LIMIT 1 [["name", "James Bond"], ["licence_to_kill", "t"]]

Certo, puoi andare pazzo con questo, ma penso che perda il suo fascino e la sua utilità se vai oltre due attributi:

Rails

Agent.find_by_name_and_licence_to_kill_and_womanizer_and_gambler_and_number_of_gadgets ('James Bond', vero, vero, vero, 3) 

SQL

SELEZIONA "agenti". * DA "agenti" DOVE "agenti". "Nome" =? AND "agenti". "Licence_to_kill" =? E "agenti". "Donnaiolo" =? E "agenti". "Giocatore d'azzardo" =? AND "agenti". "Number_of_gadgets" =? LIMIT 1 [["name", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"], ["number_of_gadgets", 3 ]]

In questo esempio, è comunque bello vedere come funziona sotto il cofano. Ogni nuovo _e_ aggiunge un SQL E operatore per legare logicamente gli attributi insieme. Nel complesso, il vantaggio principale dei finder dinamici è la leggibilità: il ridimensionamento di troppi attributi dinamici, tuttavia, perde rapidamente tale vantaggio. Uso raramente questo, forse soprattutto quando suono in console, ma è sicuramente bene sapere che Rails offre questo piccolo e semplice trucco.

Campi specifici

Active Record ti dà la possibilità di restituire oggetti che sono un po 'più focalizzati sugli attributi che portano. Di solito, se non specificato diversamente, la query richiederà tutti i campi di seguito * (SELEZIONA "agenti". *), quindi Active Record crea oggetti Ruby con il set completo di attributi. Tuttavia, puoi selezionare solo campi specifici che devono essere restituiti dalla query e limitare il numero di attributi che gli oggetti Ruby devono "portare in giro".

Rails

Agent.select ("name") => #, #,...]>

SQL

SELEZIONA "agenti". "Nome" DA "agenti"

Rails

Agent.select ("number, favorite_gadget") => #, #,...]>

SQL

SELEZIONA "agenti". "Numero", "agenti". "Preferito_gadget" DA "agenti"

Come puoi vedere, gli oggetti restituiti avranno solo gli attributi selezionati, più ovviamente i loro id, che è un dato con qualsiasi oggetto. Non fa differenza se usi le stringhe, come sopra, o simboli: la query sarà la stessa.

Rails

Agent.select (: number_of_kills) Agent.select (: name,: licence_to_kill)

Una parola di cautela: se provi ad accedere agli attributi sull'oggetto che non hai selezionato nelle tue query, riceverai a MissingAttributeError. Dal momento che il id verrà comunque fornito automaticamente per te, puoi comunque chiedere l'id senza selezionarlo.

SQL personalizzato

Ultimo ma non meno importante, puoi scrivere il tuo SQL personalizzato tramite find_by_sql. Se sei abbastanza sicuro nel tuo SQL-Fu e hai bisogno di alcune chiamate personalizzate al database, questo metodo potrebbe rivelarsi molto utile a volte. Ma questa è un'altra storia. Basta non dimenticare di verificare prima i metodi di wrapper Active Record ed evitare di reinventare la ruota in cui Rails cerca di incontrarti più di metà strada.

Rails

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("SELECT name, licence_to_kill FROM agents") 

Non sorprendentemente, questo si traduce in:

SQL

SELEZIONA * Dagli agenti SELECT name, licence_to_kill FROM agents

Poiché gli ambiti e i metodi della tua classe possono essere utilizzati in modo intercambiabile per le tue esigenze di ricerca personalizzate, possiamo fare un ulteriore passo avanti per query SQL più complesse. 

Rails

agente di classe < ActiveRecord::Base… def self.find_agent_names query = <<-SQL SELECT name FROM agents SQL self.find_by_sql(query) end end

Possiamo scrivere metodi di classe che racchiudono l'SQL all'interno di un documento Here. Questo ci consente di scrivere stringhe multi-linea in modo molto leggibile e quindi di memorizzare quella stringa SQL all'interno di una variabile che possiamo riutilizzare e passare find_by_sql. In questo modo non inseriamo tonnellate di codice di query all'interno della chiamata al metodo. Se hai più di un posto per usare questa query, è anche ASCIUTTA.

Dato che questo dovrebbe essere di facile utilizzo per i principianti e non un tutorial SQL di per sé, ho mantenuto l'esempio molto minimalista per una ragione. La tecnica per domande più complesse è comunque la stessa. È facile immaginare di avere una query SQL personalizzata lì che si estende oltre dieci righe di codice. 

Vai come pazzo di cui hai bisogno - ragionevolmente! Può essere un risparmiatore di vita. Una parola sulla sintassi qui. Il SQL parte è solo un identificatore qui per segnare l'inizio e la fine della stringa. Scommetto che non avrai più bisogno di questo metodo - speriamo! Ha sicuramente il suo posto, e Rails land non sarebbe lo stesso senza di esso - nei rari casi che vorrete assolutamente mettere a punto il vostro SQL con esso.

Pensieri finali

Spero che tu abbia ottenuto un po 'più comodo scrivere query e leggere il temuto' raw SQL '. La maggior parte degli argomenti trattati in questo articolo sono essenziali per scrivere query che trattano una logica di business più complessa. Prenditi tutto il tempo necessario per comprenderli e giocare un po 'con le query nella console. 

Sono abbastanza sicuro che quando lasci il tutorial alle spalle, prima o poi il tuo credito Rails aumenterà in modo significativo se lavori ai tuoi primi progetti di vita reale e devi creare le tue query personalizzate. Se sei ancora un po 'timido sull'argomento, direi che semplicemente ci si diverte - non è davvero una scienza missilistica!