Spørsmål i skinner, del 2

I denne andre artikkelen vil vi dykke litt dypere inn i Active Record-spørringer i Rails. Hvis du fortsatt er ny i SQL, legger jeg til eksempler som er enkle nok til at du kan merke sammen og hente syntaxen litt mens vi går. 

Når det er sagt, ville det definitivt hjelpe hvis du kjører gjennom en rask SQL-veiledning før du kommer tilbake for å fortsette å lese. Ellers ta deg tid til å forstå SQL-spørringene vi brukte, og jeg håper at ved slutten av denne serien vil det ikke føles skremmende lenger. 

Det meste av det er veldig greit, men syntaxen er litt rart hvis du nettopp startet med koding - spesielt i Ruby. Hang inn der, det er ingen rakettvitenskap!

emner

  • Inkluderer og ivrig laster
  • Sammenføyning av tabeller
  • Kraftig lasting
  • scopes
  • samlinger
  • Dynamiske Finne
  • Spesifikke felt
  • Tilpasset SQL

Inkluderer og ivrig laster

Disse spørsmålene inneholder mer enn en databasetabell for å jobbe med og kan være det viktigste å ta bort fra denne artikkelen. Det koker ned til dette: i stedet for å gjøre flere spørringer for informasjon som er spredt over flere tabeller, inkluderer prøver å holde disse til et minimum. Nøkkelbegrepet bak dette kalles "ivrig lasting" og betyr at vi laster inn tilknyttede objekter når vi finner en finne.

Hvis vi gjorde det ved å iterere over en samling objekter, og deretter prøve å få tilgang til tilhørende poster fra et annet bord, ville det føre til et problem som kalles "N + 1-spørringsproblemet". For eksempel, for hver agent.handler I en samling av agenter ville vi brenne separate søk for både agenter og deres håndtere. Det er det vi trenger å unngå siden dette ikke skaleres i det hele tatt. I stedet gjør vi følgende:

rails

agenter = Agent.includes (: handlers)

Hvis vi nå taler om en slik samling av agenter - diskontering at vi ikke har begrenset antall poster som er returnert for nå, vil vi ende opp med to spørringer i stedet for muligens en gazillion. 

SQL

SELECT "agenter". * FRA "agenter" SELECT "-håndterere". * FRA "håndtere" hvor "håndtere". "Id" IN (1, 2)

Denne ene agenten i listen har to håndtere, og når vi nå spør agent-objektet for sine håndterere, trenger ingen ekstra databasespørsmål å bli sparket. Vi kan ta dette et skritt videre, selvfølgelig, og ivrige legge inn flere tilknyttede tabelloppføringer. Hvis vi trengte å laste ikke bare håndtere, men også agentens tilknyttede oppdrag uansett grunn, kunne vi bruke inkluderer som dette.

rails

agenter = Agent.includes (: handlers,: mission)

Enkel! Bare vær forsiktig med å bruke singular og flertall versjoner for inkluderingen. De er avhengige av modellforeningene dine. EN har mange forening bruker flertall, mens a tilhører eller a has_one trenger singular versjonen, selvfølgelig. Hvis du trenger, kan du også kaste på en hvor klausul for å angi tilleggsbetingelser, men den foretrukne måten å spesifisere vilkår for tilknyttede tabeller som er ivrig innlastet, er ved å bruke tiltrer i stedet. 

En ting å huske på ivrig lasting er at dataene som vil bli lagt til, vil bli sendt tilbake i sin helhet til Active Record - som igjen bygger Ruby-objekter, inkludert disse attributter. Dette er i motsetning til "bare" å bli med i dataene, hvor du vil få et virtuelt resultat som du kan bruke til beregninger, for eksempel, og vil være mindre minneutledning enn inkluderer.

Sammenføyning av tabeller

Sammenkobling av tabeller er et annet verktøy som lar deg unngå å sende for mange unødvendige spørringer nedover rørledningen. Et vanlig scenario er å bli med to tabeller med en enkelt spørring som returnerer en slags kombinert plate. tiltrer er bare en annen finder metode for Active Record som lar deg i SQL-termer-BLI MED tabeller. Disse spørringene kan returnere poster kombinert fra flere tabeller, og du får et virtuelt bord som kombinerer poster fra disse tabellene. Dette er ganske rad når du sammenligner det for å skyte alle typer søk for hvert bord i stedet. Det er noen forskjellige typer dataoverlapping du kan få med denne tilnærmingen. 

Den indre festet er standard modus operandi for tiltrer. Dette stemmer overens med alle resultatene som samsvarer med et bestemt ID og dets representasjon som en fremmednøkkel fra et annet objekt eller bord. I eksemplet nedenfor, sett enkelt: Gi meg alle oppdrag der oppdraget er id dukker opp som mission_id i en agentens bord. "agenter". "mission_id" = "oppdrag". "id". Innerforbindelser utelukker forhold som ikke eksisterer.

rails

Mission.joins (midler):

SQL

VELG "oppdrag". * FRA "oppdrag" INNER KJØP "agenter" PÅ "agenter". "Mission_id" = "oppdrag". "Id"

Så vi matcher oppdrag og deres medfølgende agenter - i en enkelt spørring! Visst, vi kunne få oppdragene først, iterere over dem en etter en, og be om deres agenter. Men da ville vi gå tilbake til vårt fryktelige "N + 1 spørringsproblem". Nei takk! 

Det som også er fint om denne tilnærmingen er at vi ikke vil få noen tilfeller med indre sammenhenger; vi får bare poster som returneres som samsvarer med deres ids til utenlandske nøkler i tilhørende tabeller. Hvis vi trenger å finne oppdrag, for eksempel som mangler noen agenter, vil vi trenge en ytre sammenføyning i stedet. Siden dette for tiden innebærer å skrive din egen YTRE JOIN SQL, vil vi se nærmere på dette i den siste artikkelen. Tilbake til standardløyper, selvfølgelig, kan du også delta i flere tilknyttede tabeller.

rails

Mission.joins (: agenter,: utgifter,: håndtere)

Og du kan legge til på noen hvor klausuler for å spesifisere dine finnere enda mer. Nedenfor ser vi bare på oppdrag som utføres av James Bond og bare agenter som tilhører oppdraget Moonraker i det andre eksemplet.

Mission.joins (: agenter) .where (agenter: navn: 'James Bond')

SQL

VELG "oppdrag". * FRA "oppdrag" INNER JOIN "agenter" på "agenter". "Mission_id" = "oppdrag". "Id" hvor "agenter". "Navn" =? [["navn", "James Bond"]]

rails

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

SQL

VELG "agenter". * FRA "agenter" INNER JOIN "oppdrag" på "oppdrag". "Id" = "agenter". "Mission_id" WHERE "oppdrag". "Mission_name" =? [["misjonsnavn", "Moonraker"]]

Med tiltrer, Du må også være oppmerksom på singular og flertall bruk av modellforeningene dine. Fordi min Oppdrag klasse har mange: agenter, vi kan bruke flertallet. På den annen side, for Middel klasse belongs_to: mission, bare singular versjonen fungerer uten å blåse opp. Viktig liten detalj: hvor en del er enklere. Siden du søker etter flere rader i tabellen som oppfyller en viss tilstand, er flertallsformen alltid fornuftig.

scopes

Omfang er en praktisk måte å trekke ut vanlige spørringsbehov til egne navngitte metoder. På den måten er de litt enklere å passere rundt og muligens lettere å forstå om andre må jobbe med koden din, eller hvis du trenger å se nærmere på visse spørsmål i fremtiden. Du kan definere dem for enkeltmodeller, men også bruke dem for deres foreninger. 

Himmelen er grensen egentlig-tiltrer, inkluderer, og hvor er alle rettferdige spill! Siden rekkevidde også returnere Active :: Relations Objekter, du kan kjede dem og ringe andre skåler på toppen av dem uten å nøle. Å trekke ut såvidt og koble dem til mer komplekse søk, er veldig praktisk og gjør lengre de mer lesbare. Omfangene er definert via "stabby lambda" -syntaxen:

rails

klassemisjon < ActiveRecord::Base has_many: agents scope :successful, -> hvor (mission_complete: true) avslutte Mission.successful
klasse agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> hvor (womanizer: true) omfang: gambler, -> where (gambler: true) slutt # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

VELG "agenter". * FRA "agenter" hvor "agenter". "Lisence_to_kill" =? OG "agenter". "Womanizer" =? OG "agenter". "Gambler" =? [["lisence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Som du kan se fra eksempelet ovenfor, er det å finne James Bond mye bedre når du bare kan selge skåler sammen. På den måten kan du blande og matche ulike søk og holde DRY samtidig. Hvis du trenger scopes via foreninger, er de også til din disposisjon:

Mission.last.agents.licenced_to_kill.womanizer.gambler
VELG "oppdrag". * FRA "oppdrag" ORDER BY "oppdrag". "ID" DESC LIMIT 1 VELG "agenter". * FRA "agenter" hvor "agenter". "Mission_id" =? OG "agenter". "Lisence_to_kill" =? OG "agenter". "Womanizer" =? OG "agenter". "Gambler" =? [["mission_id", 33], ["lisence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Du kan også omdefinere default_scope for når du ser på noe som Mission.all.

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

SQL

 VELG "oppdrag". * FRA "oppdrag" der "oppdrag". "Status" =? [["status", "Pågår"]]

samlinger

Denne delen er ikke så mye avansert når det gjelder forståelsen, men du vil trenge dem oftere enn ikke i scenarier som kan betraktes som litt mer avanserte enn din gjennomsnittlige finner-lignende .alle, .først, .find_by_id eller hva som helst. Filtrering basert på grunnleggende beregninger, er for det meste noe som nybegynnere ikke kommer i kontakt med med en gang. Hva ser vi på akkurat her?

  • sum
  • telle
  • minimum
  • maksimum
  • gjennomsnitt

Lett peasy, ikke sant? Den kule tingen er at i stedet for å løpe gjennom en returnert samling objekter for å gjøre disse beregningene, kan vi la Active Record gjøre alt dette arbeidet for oss og returnere disse resultatene med spørringene - i ett søk helst. Hyggelig, hei?

  • telle

rails

Mission.count # => 24

SQL

VELG COUNT (*) FRA "oppdrag"
  • gjennomsnitt

rails

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

SQL

VELG AVG ("agenter". "Number_of_gadgets") FRA "agenter"

Siden vi nå vet hvordan vi kan gjøre bruk av tiltrer, Vi kan ta dette et skritt videre og bare be om det gjennomsnittet av gadgets agenter har på et bestemt oppdrag, for eksempel.

rails

Agent.joins (: mission) .where (oppdrag: navn: 'Moonraker'). Gjennomsnitt (: number_of_gadgets) .to_f # => 3,4

SQL

Velg " [["navn", "Moonraker"]]

Gruppering av disse gjennomsnittlige antall gadgets etter oppdragsnavn blir trivielt på det tidspunktet. Se mer om gruppering nedenfor:

rails

Agent.joins (: Mission) .group ( 'missions.name') gjennomsnittet. (: Number_of_gadgets)

SQL

VELG AVG ("agenter". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS oppdragnavn FRA "agenter" INNER JOIN "missioner" på "oppdrag". "Id" = "agenter". "Mission_id" GRUPPE AV oppdrag.navn
  • sum

rails

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

SQL

SELECT SUM ("agenter". "Number_of_gadgets") Fra "agenter" SELECT SUM ("agenter". "Number_of_gadgets") Fra "agenter" hvor "agenter". "License_to_kill" =? ["lisence_to_kill", "t"]] SELECT SUM ("agenter". "number_of_gadgets") Fra "agenter" hvor ("agenter". "lisens_til_kill"! =?) [["lisens_til_kill", "t"]]
  • maksimum

rails

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

SQL

SELECT MAX ("agenter". "Number_of_gadgets") Fra "agenter" SELECT MAX ("agenter". "Number_of_gadgets") FRA "agenter" hvor "agenter". "Licence_to_kill" =? [["lisence_to_kill", "t"]]
  • minimum

rails

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

SQL

SELECT MIN ("agenter". "Iq") Fra "agenter" SELECT MIN ("agenter". "Iq") FRA "agenter" hvor "agenter". "Lisence_to_kill" =? [["lisence_to_kill", "t"]]

Oppmerksomhet!

Alle disse aggregeringsmetodene lar deg ikke kjede på andre ting - de er terminale. Ordren er viktig å gjøre beregninger. Vi får ikke en Active :: Relation gjenstand fra disse operasjonene, noe som gjør at musikken stopper på det tidspunktet - vi får en hash eller tall i stedet. Eksemplene nedenfor virker ikke:

rails

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

gruppert

Hvis du vil ha beregningene brutt ned og sortert i logiske grupper, bør du gjøre bruk av a GRUPPE klausul og ikke gjør dette i Ruby. Hva jeg mener med det er at du bør unngå iterating over en gruppe som produserer potensielt mange spørsmål.

rails

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

SQL

VELG AVG ("agenter". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS oppdragnavn FRA "agenter" INNER JOIN "missioner" på "oppdrag". "Id" = "agenter". "Mission_id" GRUPPE AV oppdrag.navn

Dette eksemplet finner alle agenter som er gruppert til et bestemt oppdrag og returnerer en hash med det beregnede gjennomsnittlige antall gadgets som sine verdier - i en enkelt spørring! Jepp! Det samme gjelder også de andre beregningene selvfølgelig. I dette tilfellet er det mer sanselig å la SQL gjøre jobben. Antall spørringer vi brenner for disse aggregeringene er bare for viktig.

Dynamiske Finne

For hver egenskap på modellene dine, si Navn, epostadressefavorite_gadget og så videre, lar Active Record deg bruke svært lesbare søkemetoder som er dynamisk opprettet for deg. Høres kryptisk, jeg vet, men det betyr ikke noe annet enn find_by_id eller find_by_favorite_gadget. De find_by En del er standard, og Active Record bare tucks på navnet på attributtet for deg. Du kan til og med komme til å legge til en ! hvis du vil at søkeren skal opprette en feil hvis ingenting kan bli funnet. Den syke delen er, du kan til og med kjede disse dynamiske finnemetoder sammen. Akkurat som dette:

rails

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

SQL

VELG "agenter". * FRA "agenter" hvor "agenter". "Navn" =? LIMIT 1 [["navn", "James Bond"]] VELG "agenter". * FRA "agenter" hvor "agenter". "Navn" =? OG "agenter". "Lisence_to_kill" =? LIMIT 1 [["navn", "James Bond"], ["lisence_to_kill", "t"]]

Selvfølgelig kan du gå med dette, men jeg tror det mister sin sjarm og nytte hvis du går utover to attributter:

rails

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

SQL

VELG "agenter". * FRA "agenter" hvor "agenter". "Navn" =? OG "agenter". "Lisence_to_kill" =? OG "agenter". "Womanizer" =? OG "agenter". "Gambler" =? OG "agenter". "Number_of_gadgets" =? GRENSE 1 [["navn", "James Bond"], ["lisence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"], ["number_of_gadgets", 3 ]]

I dette eksemplet er det likevel hyggelig å se hvordan det fungerer under hetten. Hver ny _og_ legger til en SQL OG operatør for å logisk knytte egenskapene sammen. Samlet sett er den største fordelen med dynamiske finders lesbarhet, og tucking på for mange dynamiske attributter mister den fordelen raskt, skjønt. Jeg bruker sjelden dette, kanskje for det meste når jeg leker rundt i konsollen, men det er definitivt godt å vite at Rails tilbyr denne ryddige, lure.

Spesifikke felt

Active Record gir deg muligheten til å returnere objekter som er litt mer fokusert på egenskapene de bærer. Vanligvis, hvis ikke spesifisert annet, vil spørringen be om alle feltene på rad via * (VELG "agenter". *), og deretter aktiverer Active Record Ruby-objekter med det komplette settet av attributter. Men du kan å velge bare bestemte felt som skal returneres av spørringen og begrense antall attributter som dine Ruby-objekter må "bære rundt".

rails

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

SQL

VELG "agenter". "Navn" FRA "agenter"

rails

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

SQL

VELG "agenter". "Nummer", "agenter". "Favoritt_gadget" fra "agenter"

Som du kan se, vil gjenstander som returneres bare ha de valgte attributter, pluss deres ids selvfølgelig-det er en gitt med ethvert objekt. Det er ingen forskjell hvis du bruker strenger, som ovenfor, eller symboler - spørringen vil være den samme.

rails

Agent.select (: number_of_kills) Agent.select (: navn,: lisence_to_kill)

Et forsiktig ord: Hvis du prøver å få tilgang til attributter på objektet du ikke har valgt i dine spørsmål, vil du motta en MissingAttributeError. Siden id vil automatisk bli gitt til deg, uansett, du kan be om id uten å velge det, skjønt.

Tilpasset SQL

Sist men ikke minst, kan du skrive din egen tilpassede SQL via find_by_sql. Hvis du er selvsikker nok i din egen SQL-Fu og trenger noen egendefinerte samtaler til databasen, kan denne metoden til tider være veldig nyttig. Men dette er en annen historie. Ikke glem å se etter Active Record wrapper-metoder først og unngå å gjenoppfinne hjulet der Rails prøver å møte deg mer enn halvveis.

rails

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("VELG navn, lisens_til_kill FRA agenter") 

Det er overraskende at dette resulterer i:

SQL

SELECT * FRA agenter SELECT navn, lisence_to_kill FROM agenter

Siden omfang og dine egne klassemetoder kan brukes om hverandre for de tilpassede søkerbehovene dine, kan vi ta dette et skritt videre for mer komplekse SQL-spørringer. 

rails

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

Vi kan skrive klassemetoder som inkapsler SQL-en i et dokument her. Dette lar oss skrive flere linjestrenge på en veldig lesbar måte og lagre den SQL-strengen inne i en variabel som vi kan gjenbruke og sende inn find_by_sql. På den måten gipser vi ikke tonn søketekst inni metallsamtalen. Hvis du har mer enn ett sted å bruke dette spørsmålet, er det også DRY.

Siden dette er meningen å være nybegynner-vennlig og ikke en SQL-veiledning per se, holdt jeg eksemplet veldig minimalistisk av en grunn. Teknikken for langt mer komplekse spørringer er likevel den samme. Det er lett å forestille seg at du har en tilpasset SQL-spørring der som strekker seg utover ti linjer med kode. 

Gå som nøtter som du trenger - rimelig! Det kan være en livredder. Et ord om syntaksen her. De SQL del er bare en identifikator her for å markere begynnelsen og slutten av strengen. Jeg vedder på at du ikke trenger denne metoden så mye, la oss håpe! Det har definitivt sitt sted, og Rails land ville ikke være det samme uten det - i de sjeldne tilfeller at du absolutt vil finjustere din egen SQL med den.

Siste tanker

Jeg håper du har litt mer komfortable skriveforespørsler og leser den fryktede ol 'raw SQL. De fleste av emnene vi dekket i denne artikkelen er viktige for å skrive spørsmål som omhandler mer komplisert forretningslogikk. Ta deg tid til å forstå disse og leke litt med spørsmål i konsollen. 

Jeg er ganske sikker på at når du forlater opplæringsland bak, vil din Rails-kreditt før eller senere øke betydelig hvis du jobber med dine første virkelige prosjekter og trenger å lage dine egne tilpassede søk. Hvis du fortsatt er litt sjenert av emnet, vil jeg si bare ha det gøy med det - det er egentlig ingen rakettvitenskap!