Queries in Rails, parte 3

In questo ultimo pezzo, esamineremo un po 'più a fondo le domande e giocheremo con alcuni scenari più avanzati. Tratteremo più dettagliatamente i rapporti tra i modelli di Active Record in questo articolo, ma rimarrò lontano da esempi che potrebbero essere troppo confusi per la programmazione di principianti. Prima di andare avanti, cose come nell'esempio qui sotto non dovrebbero causare alcuna confusione:

Mission.last.agents.where (nome: 'James Bond')

Se non conosci le query di Active Record e SQL, ti consiglio di dare un'occhiata ai miei due articoli precedenti prima di continuare. Questo potrebbe essere difficile da digerire senza la consapevolezza che stavo accumulando finora. Fino a te, ovviamente. Il rovescio della medaglia, questo articolo non sarà lungo come gli altri se si vuole solo guardare questi casi d'uso leggermente avanzati. Scaviamo dentro! 

Temi

  • Ambiti e associazioni
  • Joins più snelli
  • fondersi
  • ha molti
  • Join personalizzati

Ambiti e associazioni

Ripeto. Possiamo interrogare subito i modelli di Active Record, ma le associazioni sono anche un gioco leale per le query e possiamo mettere in catena tutti questi elementi. Fin qui tutto bene. Possiamo impacchettare i cercatori in ambiti nitidi e riutilizzabili anche nei tuoi modelli, e ho brevemente menzionato la loro somiglianza con i metodi di classe.

Rails

agente di classe < ActiveRecord::Base belongs_to :mission scope :find_bond, -> dove (nome: 'James Bond') scope: licenced_to_kill, -> dove (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) scope: gambler, -> dove (giocatore: true) end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # = > Mission.last.agents.womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill

Quindi puoi anche impacchettarli nei tuoi metodi di classe e farli. Le sfere non sono incerte o nulla, penso - anche se la gente le menziona come un po 'magiche qua e là - ma dato che i metodi di classe ottengono la stessa cosa, opterei per quello.

Rails

agente di classe < ActiveRecord::Base belongs_to :mission def self.find_bond where(name: 'James Bond') end def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.gambler where(gambler: true) end end # => Agent.find_bond # => Agent.licenced_to_kill # => Agent.womanizer # => Agent.gambler # => Mission.last.agents.find_bond # => Mission.last.agents.licenced_to_kill # => Mission.last.agents. womanizer # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill

Questi metodi di classe leggono lo stesso, e non è necessario pugnalare qualcuno con un lambda. Qualunque cosa funzioni meglio per te o per la tua squadra; dipende da te quale API desideri utilizzare. Basta non mischiarli e abbinarli: bastoni con una sola scelta! Entrambe le versioni consentono di collegare facilmente questi metodi all'interno di un altro metodo di classe, ad esempio:

Rails

agente di classe < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer

Rails

agente di classe < ActiveRecord::Base belongs_to :mission def self.licenced_to_kill where(licence_to_kill: true) end def self.womanizer where(womanizer: true) end def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill end end # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer

Facciamo un piccolo passo avanti, rimani con me. Possiamo usare un lambda nelle associazioni stesse per definire un certo ambito. All'inizio sembra un po 'strano, ma possono essere molto utili. Ciò rende possibile chiamare questi lambda proprio sulle tue associazioni. 

Questo è abbastanza interessante e altamente leggibile con metodi più brevi che si concatenano. Attenzione però ad accoppiare questi modelli troppo stretti.

Rails

classe missione < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents

Dimmi che non è bello in qualche modo! Non è per l'uso quotidiano, ma la droga credo. Ecco Missione può "richiedere" solo agenti che hanno la licenza di uccidere. 

Una parola sulla sintassi, poiché ci siamo allontanati dalle convenzioni di denominazione e usato qualcosa di più espressivo double_o_agents. Dobbiamo menzionare il nome della classe per non confondere Rails, che altrimenti potrebbe aspettarsi di cercare una classe DoubleOAgent. Puoi, ovviamente, avere entrambi Agente le associazioni in atto, la solita e la tua abitudine, e Rails non si lamenterà.

Rails

classe missione < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.agents # => Mission.double_o_agents

Joins più snelli

Quando si esegue una query sul database per i record e non sono necessari tutti i dati, è possibile scegliere di specificare esattamente ciò che si desidera restituire. Perché? Poiché i dati restituiti a Active Record verranno infine incorporati in nuovi oggetti Ruby. Diamo un'occhiata a una semplice strategia per evitare la memoria gonfia nella tua app Rails:

Rails

classe missione < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end

Rails

Agent.all.joins (missione):

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id"

Quindi questa query restituisce un elenco di agenti con una missione dal database ad Active Record, che a sua volta si prefigge di costruire oggetti Ruby al di fuori di esso. Il missione i dati sono disponibili poiché i dati di queste righe sono uniti alle righe dei dati dell'agente. Ciò significa che i dati uniti sono disponibili durante la query ma non verranno restituiti a Record attivo. Così avrai questi dati per eseguire calcoli, per esempio. 

È particolarmente interessante perché puoi fare uso di dati che non vengono inviati alla tua app. Un numero inferiore di attributi che devono essere incorporati negli oggetti Ruby, che occupano memoria, può essere una grande vittoria. In generale, pensa di inviare solo le righe e le colonne assolute necessarie di cui hai bisogno. In questo modo puoi evitare di gonfiare un po '.

Rails

Agent.all.joins (: mission) .where (missioni: obiettivo: "Salvare il mondo")

Solo un breve accenno alla sintassi qui: perché non stiamo interrogando il Agente tavolo via dove, ma l'unito :missione tabella, dobbiamo specificare che stiamo cercando specifici missioni nel nostro DOVE clausola.

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Objective" =? [["obiettivo", "Salvare il mondo"]]

utilizzando include qui restituiremmo anche le missioni all'Active Record per il caricamento avido e occuperemo la memoria costruendo oggetti Ruby.

fondersi

UN fondersi è utile, ad esempio, quando vogliamo combinare una query sugli agenti e le relative missioni associate con un ambito specifico definito dall'utente. Possiamo prendere due ActiveRecord :: Relation oggetti e unire le loro condizioni. Certo, niente da fare, ma fondersi è utile se si desidera utilizzare un determinato ambito durante l'utilizzo di un'associazione. 

In altre parole, cosa possiamo fare con fondersi è filtrato da un ambito con nome sul modello unito. In uno degli esempi precedenti, abbiamo usato i metodi di classe per definire noi stessi tali ambiti.

Rails

classe missione < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end

Rails

Agent.joins (: missione) .merge (Mission.dangerous)

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Enemy" =? [["nemico", "Ernst Stavro Blofeld"]]

Quando incapsuliamo ciò che a pericoloso la missione è all'interno del Missione modello, possiamo infilarlo su a aderire attraverso fondersi per di qua. Quindi spostare la logica di tali condizioni sul modello pertinente a cui appartiene è da un lato una buona tecnica per ottenere un accoppiamento più flessibile - non vogliamo che i nostri modelli Active Record conoscano molti dettagli l'uno sull'altro - e sull'altro mano, ti dà una bella API nei tuoi join senza farti esplodere in faccia. L'esempio seguente senza unione non funzionerebbe senza un errore:

Rails

Agent.all.merge (Mission.dangerous)

SQL

SELEZIONA "agenti". * DA "agenti" DOVE "missioni". "Nemico" =? [["nemico", "Ernst Stavro Blofeld"]]

Quando ora uniamo un ActiveRecord :: Relation oggetto delle nostre missioni sui nostri agenti, il database non sa quali missioni stiamo parlando. Abbiamo bisogno di chiarire quale associazione abbiamo bisogno e di unire prima i dati della missione o SQL viene confuso. Un'ultima ciliegina sulla torta. Possiamo incapsulare questo ancora meglio coinvolgendo anche gli agenti: 

Rails

classe missione < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission def self.double_o_engagements joins(:mission).merge(Mission.dangerous) end end

Rails

Agent.double_o_engagements

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" WHERE "missions". "Enemy" =? [["nemico", "Ernst Stavro Blofeld"]]

È una ciliegina dolce nel mio libro. Incapsulamento, OOP appropriato e ottima leggibilità. Montepremi!

ha molti

Sopra abbiamo visto il appartiene a associazione in azione molto. Diamo un'occhiata a questo da un'altra prospettiva e introduciamo le sezioni dei servizi segreti nel mix:

Rails

sezione di classe < ActiveRecord::Base has_many :agents end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end

Quindi in questo scenario, gli agenti non avrebbero solo un mission_id ma anche a SECTION_ID. Fin qui tutto bene. Troviamo tutte le sezioni con agenti con una missione specifica, quindi sezioni che hanno una sorta di missione in corso.

Rails

Section.joins (: Agenti)

SQL

SELEZIONA "sezioni". * FROM "sezioni" INNER JOIN "agenti" ON "agenti". "Section_id" = "sezioni." Id "

Hai notato qualcosa? Un piccolo dettaglio è diverso. Le chiavi esterne sono capovolte. Qui stiamo richiedendo un elenco di sezioni ma usiamo chiavi esterne come questa: "agenti". "section_id" = "sezioni." id ". In altre parole, stiamo cercando una chiave straniera da un tavolo a cui ci stiamo unendo.

Rails

Agent.joins (missione):

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id"

In precedenza i nostri join tramite a appartiene a associazione sembrava così: le chiavi esterne erano speculari ("missioni". "id" = "agenti". "mission_id") e stavamo cercando la chiave straniera dal tavolo che stiamo iniziando.

Tornando al tuo ha molti scenario, ora otterremmo un elenco di sezioni che vengono ripetute perché hanno più agenti in ogni sezione, ovviamente. Quindi, per ogni colonna di agenti a cui ci si unisce, otteniamo una riga per questa sezione o section_id-in breve, stiamo duplicando le righe fondamentalmente. Per rendere questo ancora più vertiginoso, portiamo anche missioni nel mix.

Rails

Section.joins (agenti:: missione)

SQL

SELEZIONA "sezioni". * FROM "sezioni" INNER JOIN "agenti" ON "agenti". "Section_id" = "sezioni". "Id" INNER JOIN "missions" ON "missions". "Id" = "agents". " mission_id"

Dai un'occhiata ai due INNER JOIN parti. Ancora con me? Stiamo "raggiungendo" attraverso gli agenti le loro missioni dalla sezione dell'agente. Sì, roba per divertenti mal di testa, lo so. Ciò che otteniamo sono missioni che indirettamente si associano a una certa sezione. 

Di conseguenza, vengono aggiunte nuove colonne ma il numero di righe è sempre lo stesso restituito da questa query. Ciò che viene rimandato ad Active Record - con conseguente creazione di nuovi oggetti Ruby - è anche l'elenco delle sezioni. Quindi, quando abbiamo più missioni in corso con più agenti, otterremmo nuovamente le righe duplicate per la nostra sezione. Filtriamo ancora questo:

Rails

Section.joins (agents:: mission) .where (missioni: nemico: "Ernst Stavro Blofeld") 

SQL

SELEZIONA "sezioni". * FROM "sezioni" INNER JOIN "agenti" ON "agenti". "Section_id" = "sezioni". "Id" INNER JOIN "missions" ON "missions". "Id" = "agents". " mission_id "DOVE" missioni "." nemico "= 'Ernst Stavro Blofeld'

Ora riceviamo solo le sezioni restituite coinvolte in missioni in cui Ernst Stavro Blofeld è il nemico coinvolto. Cosmopolita come alcuni super-cattivi potrebbero pensare a se stessi, potrebbero operare in più di una sezione - dicono rispettivamente la sezione A e C, gli Stati Uniti e il Canada. 

Se abbiamo più agenti in una determinata sezione che stanno lavorando alla stessa missione per fermare Blofeld o qualsiasi altra cosa, avremmo di nuovo ripetuto le righe restituite in Active Record. Cerchiamo di essere un po 'più distinti al riguardo:

Rails

Section.joins (agenti:: missione) .where (missioni: nemico: "Ernst Stavro Blofeld"). Distinto

SQL

SELECT DISTINCT "sezioni". * FROM "sezioni" INNER JOIN "agenti" ON "agenti". "Section_id" = "sezioni". "Id" INNER JOIN "missions" ON "missions". "Id" = "agents". "mission_id" WHERE "missions". "enemy" = 'Ernst Stavro Blofeld'

Ciò che questo ci dà è il numero di sezioni che Blofeld opera - che sono note - che ha agenti attivi in ​​missioni con lui come nemico. Come ultimo passaggio, eseguiamo nuovamente il refactoring. Lo estraiamo in un bel "piccolo" metodo di classe sezione di classe:

Rails

sezione di classe < ActiveRecord::Base has_many :agents def self.critical joins(agents: :mission).where(missions:  enemy: "Ernst Stavro Blofeld" ).distinct end end class Mission < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission belongs_to :section end

Puoi refactoring ancora di più e dividere le responsabilità per ottenere un accoppiamento più sciolto, ma andiamo avanti per ora.

Join personalizzati 

La maggior parte delle volte puoi contare su Active Record per scrivere l'SQL che desideri per te. Ciò significa che rimani in Ruby land e non devi preoccuparti troppo dei dettagli del database. Ma a volte è necessario bucare un buco nella terra di SQL e fare le tue cose. Ad esempio, se è necessario utilizzare un SINISTRA aderire e uscire dal solito comportamento di Active Record di fare un INTERNO unisciti per impostazione predefinita. si unisce è una piccola finestra per scrivere il tuo SQL personalizzato, se necessario. Lo apri, inserisci il codice di query personalizzato, chiudi la "finestra" e puoi continuare ad aggiungere i metodi di query di Active Record.

Dimostriamo questo con un esempio che coinvolge i gadget. Diciamo un tipico agente di solito ha molti gadget, e vogliamo trovare agenti che non siano dotati di gadget fantastici per aiutarli sul campo. Un consueto join non produrrebbe buoni risultati dal momento che ci interessa davvero zero-o nullo in SQL parlano di questi giocattoli spia.

Rails

classe missione < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission has_many :gadgets end class Gadget < ActiveRecord::Base belongs_to :agent end

Quando facciamo a si unisce operazione, otterremo solo gli agenti restituiti che sono già dotati di gadget perché il agent_id su questi gadget non è nulla. Questo è il comportamento previsto di un join interno predefinito. Il join interno si basa su una corrispondenza su entrambi i lati e restituisce solo righe di dati che corrispondono a questa condizione. Un gadget inesistente con a zero il valore per un agente che non ha gadget non corrisponde a tale criterio. 

Rails

Agent.joins (gadget):

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "gadget" ON "gadget". "Agent_id" = "agenti". "Id"

D'altra parte, stiamo cercando agenti schmuck che hanno un disperato bisogno di un certo amore da parte del quartiermastro. La tua prima ipotesi potrebbe essere simile alla seguente:

Rails

Agent.joins (: gadget) .where (gadget: agent_id: nil) 

SQL

SELEZIONA "agenti". * DA "agenti" INNER JOIN "gadget" ON "gadget". "Agent_id" = "agenti". "Id" WHERE "gadget". "Agent_id" IS NULL

Non male, ma come puoi vedere dall'output di SQL, non suona e insiste sul default INNER JOIN. Questo è uno scenario in cui abbiamo bisogno di un ESTERNO aderire, perché manca un lato della nostra "equazione", per così dire. Stiamo cercando risultati per i gadget che non esistono, più precisamente per gli agenti senza gadget. 

Finora, quando abbiamo passato un simbolo a Active Record in un join, ci aspettavamo un'associazione. Con una stringa passata, d'altra parte, si aspetta che sia un vero frammento di codice SQL, una parte della tua query.

Rails

Agent.joins ("LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id"). Where (gadget: agent_id: nil)

SQL

SELEZIONA "agenti". * DA "agenti" LEFT OUTER JOIN gadget ON gadgets.agent_id = agents.id WHERE "gadget". "Agent_id" IS NULL

Oppure, se sei curioso di agenti pigri senza missioni, magari appesi alle Barbados o ovunque, il nostro join personalizzato sarebbe simile a questo:

Rails

Agent.joins ("LEFT OUTER JOIN missions ON missions.id = agents.mission_id"). Dove (missions: id: nil)

SQL

SELEZIONA "agenti". * DA "agenti" LEFT OUTER JOIN missioni ON missions.id = agents.mission_id WHERE "missions". "Id" IS NULL

Il join esterno è la versione di join più inclusiva poiché corrisponderà a tutti i record delle tabelle unite, anche se alcune di queste relazioni non esistono ancora. Poiché questo approccio non è esclusivo quanto i join interni, otterrai un sacco di nilli qua e là. Questo può essere informativo in alcuni casi, naturalmente, ma i join interiori sono tuttavia di solito ciò che stiamo cercando. Rails 5 ci permetterà di utilizzare un metodo specializzato chiamato left_outer_joins invece per questi casi. Finalmente! 

Una piccola cosa per la strada: mantieni questi piccoli buchi nel terreno SQL il più piccolo possibile, se puoi. Farai tutti, incluso il tuo futuro, un enorme favore.

Pensieri finali

Ottenere il record attivo per scrivere SQL efficiente per te è una delle abilità principali che dovresti togliere da questa miniserie per principianti. In questo modo, otterrai anche il codice che è compatibile con qualsiasi database supporti, il che significa che le query saranno stabili tra i database. È necessario comprendere non solo come giocare con Active Record, ma anche l'SQL sottostante, che ha la stessa importanza. 

Sì, SQL può essere noioso, noioso da leggere e non elegante, ma non dimentichiamo che Rails avvolge Active Record su SQL e non dovresti trascurare la comprensione di questo fondamentale elemento tecnologico, solo perché Rails rende molto facile non curarsene di più del tempo. L'efficienza è fondamentale per le query sui database, soprattutto se si crea qualcosa per un pubblico più ampio con traffico intenso. 

Ora vai su internet e trova altro materiale su SQL per estrarlo dal tuo sistema, una volta per tutte!