I dette siste stykket skal vi se litt nærmere på spørsmål og spille med noen mer avanserte scenarier. Vi vil dekke forholdene til Active Record-modeller litt mer i denne artikkelen, men jeg holder meg borte fra eksempler som kan være for forvirrende for programmering av nybegynnere. Før du går videre, må ting som eksempelet nedenfor ikke forårsake forvirring:
Mission.last.agents.where (navn: 'James Bond')
Hvis du er ny på Active Record-spørringer og SQL, anbefaler jeg at du tar en titt på mine to artikler før du fortsetter. Denne kan være vanskelig å svelge uten kunnskap om at jeg har bygget opp så langt. Opp til deg selvfølgelig. På forsiden vil denne artikkelen ikke være så lang som de andre hvis du bare vil se på disse litt avanserte brukstilfeller. La oss grave inn!
La oss gjenta. Vi kan spørre Active Record-modeller med en gang, men foreninger er også rettferdig spill for spørsmål - og vi kan kjede alle disse tingene. Så langt så bra. Vi kan pakke finders til fine, gjenbrukbare omfang i modellene dine også, og jeg nevnte kort deres likhet med klassemetoder.
klasse agent < ActiveRecord::Base belongs_to :mission scope :find_bond, -> where (name: 'James Bond') omfang: licenced_to_kill, -> hvor (lisens_to_kill: true) omfang: womanizer, -> where (womanizer: true) scope: gambler, sant) slutt # => 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
Så du kan også pakke dem inn i dine egne klassemetoder og bli ferdig med det. Omfangene er ikke iffy eller noe, tror jeg - selv om folk nevner dem som å være litt magiske her og der-men siden klassemetoder oppnår det samme, ville jeg velge det.
klasse agent < 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
Disse klassemetodene leser akkurat det samme, og du trenger ikke å stikke noen med en lambda. Uansett hva som passer best for deg eller ditt lag; Det er opp til deg hvilken API du vil bruke. Bare ikke bland og matche dem-hold deg med ett valg! Begge versjonene lar deg enkelt kjede disse metodene i en annen klassemetode, for eksempel:
klasse agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> hvor (womanizer: true) def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill slutten # => Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer
klasse agent < 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
La oss ta dette lille skrittet lenger - bli med meg. Vi kan bruke en lambda i foreninger til å definere et bestemt omfang. Det ser litt rart først, men de kan være ganske hendige. Det gjør det mulig å ringe disse lammene rett på dine foreninger.
Dette er ganske kult og sterkt lesbart med kortere metoder kjetting skjer. Pass på å koble disse modellene for stramt, skjønt.
klassemisjon < ActiveRecord::Base has_many :double_o_agents, -> where (license_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents
Fortell meg dette er ikke kult på en eller annen måte! Det er ikke for hverdagsbruk, men dope å gjette. Så her Oppdrag
kan "be om" bare agenter som har lisensen til å drepe.
Et ord om syntaksen, siden vi gikk bort fra navngivende konvensjoner og brukte noe mer uttrykksfulle som double_o_agents
. Vi må nevne klassenavnet for ikke å forvirre Rails, som ellers ville forvente å se etter en klasse DoubleOAgent
. Du kan selvsagt ha begge Middel
foreninger på plass-det vanlige og din egendefinerte en og Rails vil ikke klage.
klassemisjon < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (license_to_kill: true), class_name: "Agent" end # => Mission.agents # => Mission.double_o_agents
Når du spørre databasen for poster og du ikke trenger alle dataene, kan du velge å spesifisere hva du vil returnere. Hvorfor? Fordi dataene som returneres til Active Record, vil etter hvert bli bygget inn i nye Ruby Objects. La oss se på en enkel strategi for å unngå minneoppblåsthet i Rails-appen din:
klassemisjon < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.all.joins (: Mission)
VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id"
Så dette spørsmålet returnerer en liste over agenter med et oppdrag fra databasen til Active Record-som igjen setter ut for å bygge Ruby-objekter ut av det. De oppdrag
Data er tilgjengelig siden dataene fra disse radene er slått sammen på radene av agentens data. Det betyr at de sammenkomne dataene er tilgjengelige under spørringen, men vil ikke komme tilbake til Active Record. Så du vil få disse dataene til å utføre beregninger, for eksempel.
Det er spesielt kult fordi du kan bruke data som ikke blir sendt tilbake til appen din. Færre attributter som må bygges inn i Ruby-objekter, som tar opp minne, kan være en stor seier. Generelt, tenk å sende bare de absolutte nødvendige radene og kolonnene tilbake som du trenger. På den måten kan du unngå å oppblåse ganske mye.
Agent.all.joins (: mission) .where (oppdrag: objektiv: "Lagre verden")
Bare en rask side om syntaksen her: fordi vi ikke spørre Middel
bord via hvor
, men den sluttet seg :oppdrag
bord, vi må spesifisere at vi er ute etter spesifikke oppdrag
i vår HVOR
klausul.
VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id" hvor "oppdrag". [["mål", "Lagre verden"]]
Ved hjelp av inkluderer
her vil også returnere oppdrag til Active Record for ivrig lasting og ta opp minnebygging Ruby-objekter.
EN slå sammen
kommer til nytte, for eksempel når vi ønsker å kombinere en forespørsel om agenter og deres tilknyttede oppdrag som har et bestemt omfang som er definert av deg. Vi kan ta to Active :: Relation
objekter og slå sammen forholdene deres. Jo, ikke stor, men slå sammen
er nyttig hvis du vil bruke et bestemt omfang mens du bruker en tilknytning.
Med andre ord, hva vi kan gjøre med slå sammen
er filter med et navngitt omfang på den tilsluttede modellen. I et av de foregående eksemplene brukte vi klassemetoder til å definere slike navngitte omfang selv.
klassemisjon < ActiveRecord::Base has_many :agents def self.dangerous where(enemy: "Ernst Stavro Blofeld") end end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.joins (: misjon) .merge (Mission.dangerous)
VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id" WHERE "oppdrag". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
Når vi encapsulate hva a farlig
oppdrag er innenfor Oppdrag
modell, vi kan kaste den på en bli med
via slå sammen
denne måten. Så å flytte logikken til slike forhold til den aktuelle modellen der den tilhører, er på den ene siden en fin teknikk for å oppnå løstere koblinger. Vi vil ikke at våre Active Record-modeller skal kjenne mye om hverandre og på den andre hånd, det gir deg en fin API i dine sammenhenger uten å blåse opp i ansiktet ditt. Eksempelet nedenfor uten å slå sammen ville ikke fungere uten en feil:
Agent.all.merge (Mission.dangerous)
VELG "agenter". * FRA "agenter" hvor "oppdrag". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
Når vi nå smelter sammen en Active :: Relation
objekt for våre oppdrag på våre agenter, vet databasen ikke hvilke oppdrag vi snakker om. Vi må klargjøre hvilken tilknytning vi trenger og delta i oppdragsdataene først, eller SQL blir forvirret. En siste kirsebær på toppen. Vi kan inkapslere dette enda bedre ved å involvere agenter også:
klassemisjon < 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
Agent.double_o_engagements
VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id" WHERE "oppdrag". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
Det er litt søt kirsebær i boken min. Innkapsling, riktig OOP, og god lesbarhet. jackpot!
Over vi har sett tilhører
forening i aksjon mye. La oss se på dette fra et annet perspektiv og ta med hemmelige serviceavsnitt i blandingen:
klasseseksjon < 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
Så i dette scenariet ville agenter ikke bare ha en mission_id
men også a SECTION_ID
. Så langt så bra. La oss finne alle seksjoner med agenter med et bestemt oppdrag - så seksjoner som har en slags oppgave som skjer.
Section.joins (midler):
VELG "seksjoner". * FRA "seksjoner" INNER JOIN "agenter" PÅ "agenter". "Section_id" = "sections." Id "
Har du lagt merke til noe? En liten detalj er annerledes. De fremmede nøklene er vendt. Her ber vi om en liste over seksjoner, men bruk utenlandske nøkler som dette: "agenter". "section_id" = "sections." id "
. Med andre ord, vi leter etter en fremmed nøkkel fra et bord vi går med.
Agent.joins (: Mission)
VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id"
Tidligere har vi inngått via a tilhører
foreningen så slik ut: de fremmede nøklene ble speilet ("oppdrag". "id" = "agenter". "mission_id"
) og leter etter den utenlandske nøkkelen fra bordet vi starter.
Kommer tilbake til din har mange
scenario vil vi nå få en liste over seksjoner som gjentas fordi de har flere agenter i hver seksjon, selvfølgelig. Så for hver agent kolonne som blir slått på, får vi en rad for denne delen eller section_id-kort sagt, vi dupliserer rader i utgangspunktet. For å gjøre dette enda mer svimlende, la oss også ta med oppdrag i blandingen.
Section.joins (agenter:: oppdrag)
SELECT "sections". * FRA "seksjoner" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". mission_id"
Sjekk ut de to INNER JOIN
deler. Fortsatt med meg? Vi er "nå" gjennom agenter til deres oppdrag fra agentens seksjon. Ja, ting for morsomme hodepine, vet jeg. Det vi får er oppdrag som indirekte forbinder med en bestemt del.
Som et resultat får vi nye kolonner med på, men antall rader er fortsatt det samme som blir returnert av denne spørringen. Hva som sendes tilbake til Active Record, som resulterer i å bygge nye Ruby-objekter, er også fortsatt listen over seksjoner. Så når vi har flere oppdrag som skjer med flere agenter, vil vi få dupliserte rader for vår seksjon igjen. La oss filtrere dette litt mer:
Section.joins (agenter:: oppdrag) .where (oppdrag: fiende: "Ernst Stavro Blofeld")
SELECT "sections". * FRA "seksjoner" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". mission_id "WHERE" oppdrag "." fiende "= 'Ernst Stavro Blofeld'
Nå får vi bare seksjoner som er involvert i oppdrag der Ernst Stavro Blofeld er den involverte fienden. Kosmopolitiske som noen superskurker kan tenke på seg selv, de kunne operere i mer enn en seksjon - si seksjoner A og C, henholdsvis United Sates og Canada.
Hvis vi har flere agenter i en gitt seksjon som jobber med samme oppgave for å stoppe Blofeld eller hva som helst, ville vi igjen ha gjentatte rader tilbake til oss i Active Record. La oss være litt mer tydelige om det:
Section.joins (agenter:: oppdrag) .where (oppdrag: fiende: "Ernst Stavro Blofeld").
SELECT DISTINCT "seksjoner". * FRA "seksjoner" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "missioner" på "oppdrag". "Id" = "agenter". "mission_id" WHERE "oppdrag". "fiende" = 'Ernst Stavro Blofeld'
Hva dette gir oss er antall seksjoner som Blofeld driver ut av - som er kjent - som har agenter som er aktive i oppdrag med ham som fiende. Som et siste skritt, la oss gjøre noen refactoring igjen. Vi trekker dette inn i en fin "liten" klassemetode på klasseseksjon
:
klasseseksjon < 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
Du kan refactor dette enda mer og splitte opp ansvaret for å oppnå løsere kobling, men la oss gå videre for nå.
Mesteparten av tiden kan du stole på Active Record å skrive SQL som du vil ha for deg. Det betyr at du bor i Ruby land og ikke trenger å bekymre deg for databasen detaljer for mye. Men noen ganger må du kaste et hull i SQL-land og gjøre dine egne ting. For eksempel, hvis du trenger å bruke en VENSTRE
bli med og bryte ut av Active Records vanlige oppførsel å gjøre en INDRE
bli med som standard. tiltrer
er et lite vindu for å skrive din egen tilpassede SQL hvis nødvendig. Du åpner den, kobler inn din egendefinerte spørrekode, lukker "vinduet" og kan fortsette å legge til i Active Record-spørringsmetoder.
La oss demonstrere dette med et eksempel som involverer gadgets. La oss si en typisk agent som regel har mange
gadgets, og vi vil finne agenter som ikke er utstyrt med noe fancy gadgetry for å hjelpe dem i feltet. En vanlig medvirkning gir ikke gode resultater siden vi faktisk er interessert i nil
-eller null
i SQL-taleværdier av disse spionlegene.
klassemisjon < 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
Når vi gjør en tiltrer
operasjon, vil vi få kun agenter tilbake som allerede er utstyrt med gadgets fordi agent_id
på disse gadgetene er ikke null. Dette er den forventede oppførselen til en standard indre sammenføyning. Den indre festet bygger på en kamp på begge sider og returnerer kun rader med data som samsvarer med denne tilstanden. En ikke-eksisterende gadget med a nil
verdien for en agent som ikke har noen gadget, stemmer ikke overens med dette kriteriet.
Agent.joins (: gadgets)
VELG "agenter". * FRA "agenter" INNER GÅ TIL "gadgets" på "gadgets". "Agent_id" = "agenter". "Id"
Vi er derimot på jakt etter schmuck-agenter som har behov for litt kjærlighet fra kvartemesteren. Din første gjetning kan ha sett ut som følgende:
Agent.joins (: gadgets) .where (gadgets: agent_id: nil)
VELG "agenter". * FRA "agenter" INNER BLI MED "Gadgets" PÅ "Gadgets". "Agent_id" = "Agenter". "ID" HVOR "Gadgets". "Agent_id" ER NULL
Ikke dårlig, men som du kan se fra SQL-utgangen, spiller den ikke sammen og fortsatt insisterer på standardinnstillingen INNER JOIN
. Det er et scenario der vi trenger en YTRE
bli med, fordi en side av vår "likning" mangler, så å si. Vi leter etter resultater for gadgets som ikke eksisterer-mer presist, for agenter uten gadgets.
Så langt, da vi passerte et symbol til Active Record i en sammenheng, ventet det en forening. Med en streng passert, forventer den at det er et faktisk fragment av SQL-kode - en del av søket ditt.
Agent.joins ("LEFT OUTER JOIN gadgets ON gadgets.agent_id = agents.id"). Hvor (gadgets: agent_id: nil)
VELG "agenter". * FRA "agenter" VENSTRE YTRE JOIN-GIRLGER PÅ Gadgets.agent_id = agenter.id HVOR "Gadgets". "Agent_id" ER NULL
Eller, hvis du er nysgjerrig på lat agenter uten oppdrag-hengende muligens i Barbados eller hvor som helst - vår tilpassede delta vil se slik ut:
Agent.joins ("LEFT OUTER JOIN-oppdrag på missions.id = agents.mission_id"). Hvor (oppdrag: id: nil)
VELG "agenter". * FRA "agenter" LEFT OUTER JOIN-oppdrag PÅ missions.id = agents.mission_id HVOR "oppdrag". "ID" ER NULL
Den ytre sammenføyningen er den mer inkluderende deltaversjonen, siden den vil samsvare med alle poster fra de medfølgende tabellene, selv om noen av disse relasjonene ikke eksisterer ennå. Fordi denne tilnærmingen ikke er like eksklusiv som indre sammenhenger, får du en masse nils her og der. Dette kan være informativ i noen tilfeller, selvfølgelig, men indre sammenhenger er likevel vanligvis det vi leter etter. Rails 5 vil la oss bruke en spesialisert metode som heter left_outer_joins
i stedet for slike tilfeller. Endelig!
En liten ting for veien: Hold disse hullene i SQL-land så lite som mulig hvis du kan. Du vil gjøre alle sammen - inkludert din fremtid selv - en enorm tjeneste.
Å få Active Record til å skrive effektiv SQL for deg er en av de viktigste ferdighetene du bør ta bort fra denne mini-serien for nybegynnere. På denne måten vil du også få kode som er kompatibel med hvilken database den støtter - noe som betyr at spørringene vil være stabile på tvers av databaser. Det er nødvendig at du ikke bare forstår hvordan du spiller med Active Record, men også den underliggende SQL, som er like viktig.
Ja, SQL kan være kjedelig, kjedelig å lese og ikke elegant utseende, men ikke glem at Rails wraps Active Record rundt SQL, og du bør ikke forsømme å forstå denne viktige teknologien, bare fordi Rails gjør det veldig enkelt å ikke bry seg mest av tiden. Effektivitet er avgjørende for databasespørsmål, spesielt hvis du bygger noe for større publikum med stor trafikk.
Gå nå ut på internettene og finn litt mer materiale på SQL for å få det ut av systemet - en gang for alle!