Interogări în rails, Partea 3

În această ultimă piesă, vom arăta un pic mai adânc în întrebări și vom juca cu câteva scenarii mai avansate. Vom acoperi relațiile dintre modelele Active Record un pic mai mult în acest articol, dar voi sta departe de exemple care ar putea fi prea confuze pentru programarea începătorilor. Înainte de a vă deplasa înainte, lucruri precum exemplul de mai jos nu trebuie să provoace confuzie:

Mission.last.agents.where (nume: "James Bond")

Dacă sunteți nou în interogările Active Record și SQL, vă recomandăm să aruncați o privire la cele două articole anterioare înainte de a continua. Aceasta ar putea fi greu de înghițit fără cunoștințele pe care le-am construit până acum. Până la tine, desigur. Pe de altă parte, acest articol nu va fi la fel de lung ca și celelalte, dacă doriți doar să vă uitați la aceste cazuri de utilizare ușoară. Hai să intrăm! 

Subiecte

  • Scopes & Asociații
  • Slimmer se alătură
  • contopi
  • are multe
  • Conectarea personalizată

Scopes & Asociații

Să reiteream. Putem interoga imediat modelele Active Record, dar asociațiile sunt, de asemenea, un joc echitabil pentru întrebări - și putem lanțuri toate aceste lucruri. Până acum, bine. Putem să găsim pachete de identificatori în domenii de utilizare reutilizabile și în modelele dvs., și am menționat în scurt timp similitudinea lor cu metodele de clasă.

Șine

Agent de clasă < ActiveRecord::Base belongs_to :mission scope :find_bond, -> unde (nume: "James Bond") domeniul de aplicare: licenced_to_kill, -> where (license_to_kill: true) domeniu: femeie, adevărat) 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

Deci, puteți să le împachetați și în metodele proprii de clasă și să le faceți cu ele. Scopes nu sunt iffy sau altceva, cred că-deși oamenii le menționează ca fiind un pic de magie aici și acolo-dar, deoarece metodele de clasă atinge același lucru, aș opta pentru asta.

Șine

Agent de clasă < 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. femeie # => Mission.last.agents.gambler # => Agent.licenced_to_kill.womanizer.gambler # => Mission.last.agents.womanizer.gambler.licenced_to_kill

Aceste metode de clasă citesc exact același lucru și nu trebuie să înjunghiați pe nimeni cu o lambda. Orice lucreaza cel mai bine pentru tine sau pentru echipa ta; depinde de tine ce API doriți să utilizați. Doar nu amestecați-le și le potriviți-stick cu o singură alegere! Ambele versiuni vă permit să legeți cu ușurință aceste metode într-o altă metodă de clasă, de exemplu:

Șine

Agent de clasă < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (license_to_kill: true) domeniu: feminin, -> unde (femeie: adevărat) 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

Șine

Agent de clasă < 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

Hai să facem un mic pas mai departe - stai cu mine. Putem folosi o lambda în asociații pentru a defini un anumit domeniu. Pare puțin ciudat la început, dar ele pot fi destul de la îndemână. Acest lucru face posibilă numirea acestor lambe-uri la asociațiile dvs.. 

Acest lucru este destul de rece și foarte ușor de citit, cu metode mai scurte de înlănțuire continuă. Feriți-vă de cuplarea acestor modele prea strânse, totuși.

Șine

clasa Misiune < ActiveRecord::Base has_many :double_o_agents, -> where (license_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents

Spune-mi că nu e bine cumva! Nu este pentru uzul cotidian, dar bănuiește să cred. Deci aici Misiune pot "cere" numai agenți care au licența de a ucide. 

Un cuvânt despre sintaxa, deoarece ne-am îndepărtat de convențiile de numire și am folosit ceva mai expresiv double_o_agents. Trebuie să menționăm numele clasei pentru a nu confunda Rails, care altfel ar putea aștepta să caute o clasă DoubleOAgent. Puteți, bineînțeles, să aveți și amândouă Agent asociațiile în vigoare - cele obișnuite și cele personalizate - iar Rails nu se va plânge.

Șine

clasa Misiune < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (license_to_kill: true), nume_clasă: "Agent" end # => Mission.agents # => Mission.double_o_agents

Slimmer se alătură

Când interogați baza de date pentru înregistrări și nu aveți nevoie de toate datele, puteți alege să specificați exact ce doriți să vi se returneze. De ce? Deoarece datele returnate la Active Record vor fi în cele din urmă construite în obiecte Ruby noi. Să ne uităm la o strategie simplă pentru a evita inflația de memorie în aplicația Rails:

Șine

clasa Misiune < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end

Șine

Agent.all.joins (misiune):

SQL

SELECT "agenți". * FROM "agenți" INNER JOIN "misiuni" ON "misiuni" "id" = "

Deci, această interogare returnează o listă de agenți cu o misiune din baza de date către Active Record - care apoi, la rândul ei, se dorește să construiască obiecte Ruby din ea. misiune datele sunt disponibile deoarece datele din aceste rânduri se alătură rândurilor datelor agentului. Aceasta înseamnă că datele integrate sunt disponibile în timpul interogării, dar nu vor fi returnate la Active Record. Deci, veți avea aceste date pentru a efectua calcule, de exemplu. 

Este deosebit de interesant, deoarece puteți utiliza datele care nu mai sunt trimise înapoi în aplicația dvs. Mai puține atribute care trebuie să fie construite în obiecte Ruby - care preiau memoria - pot fi o mare victorie. În general, gândiți-vă să trimiteți numai rândurile și coloanele absolut necesare înapoi de care aveți nevoie. În acest fel, puteți evita o bătaie destul de puțin.

Șine

Agent.all.joins (: mission) .where (misiuni: objective: "Salvarea lumii")

Doar o scurtă privire la sintaxa de aici: pentru că nu interogăm Agent tabel prin Unde, dar cei adunați :misiune tabel, trebuie să precizăm că căutăm anumite misiuni în a noastră UNDE clauză.

SQL

SELECT "agenți". * FROM "agenți" INNER JOIN "misiuni" "ON" misiuni "" id "=" [["obiectiv", "salvarea lumii"]]

Utilizarea include aici s-ar întoarce, de asemenea, misiuni în Active Record pentru încărcare dornică și să preia obiecte de Ruby de memorie.

contopi

A îmbina vine la îndemână, de exemplu, atunci când dorim să combinăm o interogare cu agenții și misiunile asociate care au un anumit domeniu de aplicare definit de dvs. Putem lua două ActiveRecord :: Relația obiecte și îmbinarea condițiilor lor. Sigur, nici un biggie, dar îmbina este util dacă doriți să utilizați un anumit domeniu în timp ce utilizați o asociere. 

Cu alte cuvinte, cu ce putem face îmbina este filtrat de un domeniu denumit pe modelul îmbinat. Într-unul din exemplele anterioare, am folosit metode de clasă pentru a defini noi astfel de domenii.

Șine

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

Șine

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

SQL

SELECT "agenții". * FROM "agenți" INNER JOIN "misiuni" "ON" misiuni "" id "=" [["inamic", "Ernst Stavro Blofeld"]]

Când încapsulem ce a periculos misiunea este în cadrul Misiune model, îl putem pune pe un a adera prin intermediul îmbina pe aici. Așadar, mișcarea logicii unor astfel de condiții la modelul relevant în care aparține este, pe de o parte, o tehnică frumoasă pentru a realiza cuplarea mai relaxată - nu vrem ca modelele noastre Active Record să cunoască o mulțime de detalii despre unul pe altul - și pe de altă parte de mână, vă oferă un API frumos în aderarea dvs. fără a sufla în fața dumneavoastră. Exemplul de mai jos fără fuzionare nu ar funcționa fără o eroare:

Șine

Agent.all.merge (Mission.dangerous)

SQL

SELECȚI "agenți". * FROM "agenți" WHERE "misiuni". "Inamic" =? [["inamic", "Ernst Stavro Blofeld"]]

Atunci când fuzionăm acum ActiveRecord :: Relația obiecte pentru misiunile noastre pe agenții noștri, baza de date nu știe despre care misiuni vorbim. Trebuie să clarificăm care este asocierea de care avem nevoie și să aderăm mai întâi la datele misiunii - sau SQL devine confuz. Un ultim cireș deasupra. Putem încapsula acest lucru și mai bine prin implicarea agenților, de asemenea: 

Șine

clasa Misiune < 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

Șine

Agent.double_o_engagements

SQL

SELECT "agenții". * FROM "agenți" INNER JOIN "misiuni" "ON" misiuni "" id "=" [["inamic", "Ernst Stavro Blofeld"]]

Asta eo cireșă dulce în cartea mea. Încapsulare, OOP propriu-zisă și o mare lizibilitate. potul cel mare!

are multe

Mai sus am văzut aparține lui asociere în acțiune foarte mult. Să aruncăm o privire la aceasta dintr-o altă perspectivă și să aducem secțiuni de servicii secrete în mix:

Șine

clasă < 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

Deci, în acest scenariu, agenții ar avea nu numai un mission_id dar și a SECTION_ID. Până acum, bine. Să găsim toate secțiunile cu agenți cu o anumită misiune - deci secțiuni care au un fel de misiune.

Șine

Section.joins (: agenți)

SQL

SELECT "sectiuni" * FROM "sectiuni" agenti INNER JOIN "agenti" ON "" sectiuni "section_id" = "

Ai observat ceva? Un mic detaliu este diferit. Cheile străine sunt rotite. Aici solicităm o listă de secțiuni, dar folosim chei străine, cum ar fi: "agenți". "secțiunea_id" = "secțiuni" id ". Cu alte cuvinte, căutăm o cheie străină de la o masă pe care o aderăm.

Șine

Agent.joins (misiune):

SQL

SELECT "agenți". * FROM "agenți" INNER JOIN "misiuni" ON "misiuni" "id" = "

Anterior ne-am alăturat printr-un aparține lui Asociația arăta astfel: cheile străine au fost reflectate ("misiuni" "id" = "agenți". "mission_id") și au căutat cheia străină de la masa pe care o pornim.

Revenind la dvs. are multe scenariu, acum vom obține o listă de secțiuni care se repetă deoarece au mai mulți agenți în fiecare secțiune, desigur. Deci, pentru fiecare coloană de agenți care se îmbină, obținem un rând pentru această secțiune sau secțiune_id-pe scurt, redăm în mod substanțial rânduri. Pentru a face acest lucru și mai amețitor, să aducem misiuni și în mix.

Șine

Section.joins (agenți:: misiune)

SQL

SELECT "sectiuni" * FROM "sectiuni" INNER JOIN "agenti" ON "agenti" "section_id" = "sectiuni". "Id" INNER JOIN "misiuni" ON " mission_id“

Verifică cele două INNER JOIN părți. Încă cu mine? Noi "ajungem" prin intermediul agenților la misiunile lor din secțiunea agentului. Da, chestii pentru dureri de cap, știu. Ceea ce obținem sunt misiuni care se asociază indirect cu o anumită secțiune. 

Ca rezultat, primim noi coloane, dar numărul de rânduri este același, care este returnat de această interogare. Ceea ce este trimis înapoi la Active Record - rezultând în construirea de noi obiecte Ruby - este, de asemenea, în continuare lista secțiunilor. Deci, atunci când avem mai multe misiuni care se întâmplă cu mai mulți agenți, vom primi din nou secțiuni duplicate pentru secțiunea noastră. Să mai filtrăm acest lucru:

Șine

(Misiuni: inamic: "Ernst Stavro Blofeld") 

SQL

SELECT "sectiuni" * FROM "sectiuni" INNER JOIN "agenti" ON "agenti" "section_id" = "sectiuni". "Id" INNER JOIN "misiuni" ON " mission_id "WHERE" misiuni "." inamic "=" Ernst Stavro Blofeld "

Acum primim numai secțiuni returnate care sunt implicate în misiuni în care Ernst Stavro Blofeld este inamicul implicat. Cosmopolitan, deoarece unii răufăcători superiori s-ar putea gândi la ei înșiși, ar putea opera în mai multe secțiuni - să zicem secțiunile A și C, Statele Unite și, respectiv, Canada. 

Dacă avem mai mulți agenți într-o anumită secțiune care lucrează la aceeași misiune pentru a opri Blofeld sau orice altceva, vom reveni din nou la noi în Active Record. Hai să fim puțin mai distinși în privința asta:

Șine

(Misiuni: inamic: "Ernst Stavro Blofeld".

SQL

SELECT DISTINCT "secțiunile" * FROM "secțiunile" INNER JOIN "agenți" ON "agenți". "Id" = "agenți". "mission_id" WHERE "misiuni". "inamic" = "Ernst Stavro Blofeld"

Ceea ce ne oferă acest lucru este numărul de secțiuni pe care Blofeld o exploatează - care sunt cunoscute - care au agenți activi în misiuni cu el ca dușman. Ca ultim pas, să mai facem din nou refactorizarea. Extragem acest lucru într-o metodă de clasă "mică" clasă:

Șine

clasă < 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

Puteți să refaceți acest aspect și să vă împărțiți responsabilitățile pentru a realiza cuplarea mai relaxată, dar să mergem mai departe.

Conectarea personalizată 

De cele mai multe ori puteți să vă bazați pe înregistrarea activă scriind SQL pe care doriți pentru dvs. Asta inseamna ca stati in teren Ruby si nu trebuie sa va ingrijorati prea mult de detaliile bazei de date. Dar, uneori, trebuie să dați o gaură în teren SQL și să faceți propriul dvs. lucru. De exemplu, dacă trebuie să utilizați a STÂNGA să se alăture și să iasă din comportamentul obișnuit al lui Active Record de a face o INTERIOR se alăture implicit. se alătură este o fereastră mică pentru a vă scrie propriul SQL personalizat dacă este necesar. Deschideți-l, introduceți codul de interogare personalizat, închideți fereastra și puteți continua să adăugați metode de interogare Active Record.

Să demonstram acest lucru cu un exemplu care implică gadget-uri. Să spunem de obicei un agent tipic are multe gadget-uri, și vrem să găsim agenți care nu sunt echipați cu niciun fel de gadgeturi fantezie care să le ajute pe câmp. O adunare obișnuită nu ar aduce rezultate bune, de vreme ce ne interesează zero-sau nul în valorile de vorbire SQL ale acestor jucării de spionaj.

Șine

clasa Misiune < 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

Când facem a se alătură operațiune, vom primi numai agenți returnați care sunt deja echipați cu gadgeturi deoarece agent_id pe aceste gadget-uri nu este zero. Acesta este comportamentul așteptat al unei intrări interne implicite. Îmbinarea interioară se bazează pe un meci de pe ambele părți și returnează numai rânduri de date care se potrivesc cu această condiție. Un obiect gadget inexistent cu un zero valoarea pentru un agent care nu poartă nici un obiect gadget nu se potrivește cu acest criteriu. 

Șine

Agent.joins (: gadget-uri)

SQL

SELECT "agenți". * FROM "agenți" INNER JOIN "gadget-uri" ON "gadgets" "agent_id" = "

Suntem, pe de altă parte, în căutarea unor agenți schmuck care au nevoie de puțină dragoste de la conducătorul de carieră. Prima dvs. estimare ar fi putut să arate după cum urmează:

Șine

Agent.joins (: gadget-uri) .de unde (gadget-uri: agent_id: nil) 

SQL

SELECT "agenții". * FROM "agenți" INNER JOIN "gadget-uri" ON "gadget-uri" "agent_id" = "agenți" "id" WHERE "gadgets"

Nu este rău, dar după cum puteți vedea din ieșirea SQL, nu se joacă de-a lungul și încă insistă asupra implicit INNER JOIN. Acesta este un scenariu în care avem nevoie de unul EXTERIOR alăturați-vă, deoarece o parte a "ecuației" noastre lipsește, ca să spunem așa. Căutăm rezultate pentru gadget-uri care nu există - mai precis pentru agenți fără gadgeturi. 

Până acum, când am trecut un simbol la Active Record într-un concurs, se aștepta la o asociere. Cu un șir transmis, în schimb, se așteaptă ca acesta să fie un fragment real al codului SQL - o parte a interogării dvs..

Șine

Agent.joins ("gadgets la ieșire din stânga pe gadgets.agent_id = agents.id") unde (gadgets: agent_id: nil)

SQL

SELECT "agenți". * FROM "agenți" Lăsați gadget-urile OUT OUTER JOIN ON gadgets.agent_id = agents.id Unde "gadgets". "Agent_id" IS NULL

Sau, dacă sunteți curioși de agenții leneși fără misiuni - agățat, eventual, în Barbados sau oriunde s-ar potrivi personajul nostru ar arăta astfel:

Șine

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

SQL

SELECT "agenți". * FROM "agenți" LEFT OUTOUN JOIN misiuni ON missions.id = agents.mission_id WHERE "misiuni". "Id" IS NULL

Asamblarea exterioară este versiunea mai cuprinzătoare de conectare, deoarece se va potrivi cu toate înregistrările din tabelele integrate, chiar dacă unele dintre aceste relații nu există încă. Deoarece această abordare nu este la fel de exclusivă ca și cea interioară, veți obține o grămadă de nils aici și acolo. Acest lucru poate fi informativ în unele cazuri, desigur, dar legăturile interioare sunt, totuși, de obicei ceea ce căutăm. Rails 5 ne va permite să folosim o metodă specializată numită left_outer_joins în loc de astfel de cazuri. In cele din urma! 

Un lucru mic pentru drum: păstrați aceste găuri de peeping în SQL teren cât mai mic posibil, dacă puteți. Veți face pe toată lumea - inclusiv pe viitorul vostru - o favoare extraordinară.

Gândurile finale

Obținerea înregistrării active pentru a scrie SQL eficient pentru tine este una din competențele principale pe care ar trebui să le îndepărtați de la această mini-serie pentru începători. În acest fel, veți obține și codul compatibil cu baza de date pe care o suportă - ceea ce înseamnă că interogările vor fi stabile în cadrul bazelor de date. Este necesar să înțelegeți nu numai cum să jucați cu Active Record, ci și SQL, care are o importanță egală. 

Da, SQL poate fi plictisitor, plictisitor de citit si nu arata elegant, dar nu uita ca Rails inregistreaza Active Record in jurul SQL-ului si nu trebuie sa neglijezi intelegerea acestei tehnologii vitale - doar pentru ca Rails face foarte usor sa nu ai grija cel mai mult din timp. Eficiența este esențială pentru interogările bazei de date, mai ales dacă construiți ceva pentru publicul larg cu trafic intens. 

Acum ieșiți pe internet și găsiți mai multe materiale pe SQL pentru al scoate din sistem - odată pentru totdeauna!

Cod