Frågor i Rails, Del 2

I den här andra artikeln dyker vi lite djupare in i Active Record-frågor i Rails. Om du fortfarande är ny för SQL, lägger jag till exempel som är enkla att du kan tagga med och hämta syntaxen lite när vi går. 

Med det sagt skulle det definitivt hjälpa om du kör igenom en snabb SQL-handledning innan du kommer tillbaka för att fortsätta läsa. Annars tar du dig tid att förstå SQL-frågorna vi använde, och jag hoppas att det i slutet av denna serie inte kommer att känna sig skrämmande längre. 

Det mesta är verkligen enkelt, men syntaxen är lite konstig om du bara började med kodning - särskilt i Ruby. Häng in där, det är ingen raketvetenskap!

ämnen

  • Innehåller & Ivriga Laddar
  • Ansluta tabeller
  • Ivriga Laddar
  • Scopes
  • aggregeringar
  • Dynamiska Finders
  • Specifika fält
  • Anpassad SQL

Innehåller & Ivriga Laddar

Dessa frågor innehåller mer än en databas tabell att arbeta med och kan vara det viktigaste att ta bort från den här artikeln. Det pekar på detta: i stället för att göra flera frågor för information som sprids över flera tabeller, innefattar försöker hålla dessa till ett minimum. Nyckelbegreppet bakom detta kallas "ivrig lastning" och innebär att vi laddar in tillhörande objekt när vi gör en sökning.

Om vi ​​gjorde det genom att iterera över en samling objekt och försökte få åtkomst till dess tillhörande poster från ett annat bord, skulle vi komma fram till ett problem som kallas "N + 1-frågaproblemet". Till exempel, för varje agent.handler I en samling agenter skulle vi skjuta separata frågor för både agenter och deras hanterare. Det är vad vi behöver för att undvika, eftersom det inte är så omfattande. I stället gör vi följande:

rails

agenter = Agent.includes (: handlers)

Om vi ​​nu härskar över en sådan samling agenter-diskontering att vi inte har begränsat antalet poster som återlämnats för nu, slutar vi med två frågor i stället för eventuellt en gazillion. 

SQL

VÄLJ "agenter". * FRÅN "agenter" SELECT "hanterare". * FRÅN "hanterare" VAR "hanterare". "Id" IN (1, 2)

Den här agenten i listan har två hanterare, och när vi nu frågar agenten för sina hanterare behöver inga ytterligare databasfrågor avfyra. Vi kan ta det här ett steg längre, självklart, och ivrigt laddar flera associerade tabellposter. Om vi ​​behövde ladda inte bara hanterare utan även agentens tillhörande uppdrag av någon anledning, skulle vi kunna använda innefattar så här.

rails

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

Enkel! Var bara försiktig om att använda singular och pluralversioner för innehållet. De beror på dina modellföreningar. en har många association använder plural, medan a tillhör eller a har en behöver den singulära versionen, förstås. Om du behöver, kan du också klibba på en var klausul för att ange ytterligare villkor, men det föredragna sättet att ange villkor för tillhörande tabeller som är ivriga laddade är att använda förenar istället. 

En sak att komma ihåg om ivriga laddningar är att de data som läggs till kommer att skickas tillbaka till fullo till Active Record - som i sin tur bygger Ruby-objekt inklusive dessa attribut. Detta står i motsats till att "helt enkelt" gå med i data, där du får ett virtuellt resultat som du kan använda för beräkningar, till exempel, och kommer att bli mindre minnesdränering än inkluderar.

Ansluta tabeller

Att ansluta tabeller är ett annat verktyg som låter dig undvika att skicka för många onödiga frågor ner i rörledningen. Ett vanligt scenario går samman i två tabeller med en enda fråga som returnerar en sorts kombinerad post. förenar är bara en annan sökarmetod för Active Record som låter dig-i SQL-termer-ANSLUTA SIG tabeller. Dessa frågor kan returnera poster i kombination med flera tabeller, och du får ett virtuellt bord som kombinerar poster från dessa tabeller. Det här är ganska rad när du jämför det med att skjuta alla typer av frågor för varje tabell istället. Det finns några olika typer av dataöverlapp som du kan få med detta tillvägagångssätt. 

Den inre föreningen är standardmetoden operandi för förenar. Detta matchar alla resultat som matchar ett visst ID och dess representation som en främmande nyckel från ett annat objekt eller bord. I exemplet nedan, sätt helt enkelt: ge mig alla uppdrag där uppdraget är id visas som mission_id i en ombuds bord. "agenter". "mission_id" = "missions". "id". Inre föreningar utesluter relationer som inte existerar.

rails

Mission.joins (: medel)

SQL

VÄLJ "uppdrag". * FRÅN "uppdrag" INNER JOIN "agenter" på "agenter". "Mission_id" = "mission". "Id"

Så vi matchar uppdrag och deras medföljande agenter - i en enda fråga! Visst, vi kan få uppdrag först, iterera över dem en efter en, och be om deras agenter. Men då skulle vi gå tillbaka till vårt fruktansvärda "N + 1-frågaproblem". Nej tack! 

Det som också är trevligt med detta tillvägagångssätt är att vi inte kommer att få några nollfall med inre föreningar. vi får bara poster som returneras som matchar deras ids till utländska nycklar i tillhörande tabeller. Om vi ​​behöver hitta uppdrag, till exempel, som saknar några agenter, skulle vi behöva ett yttre drag i stället. Eftersom detta för närvarande innebär att du skriver din egen YTTRE JOIN SQL, vi kommer att titta på detta i den senaste artikeln. Tillbaka till standardförbindelser kan du självklart också ansluta till flera associerade tabeller.

rails

Mission.joins (: agenter,: utgifter,: hanterare)

Och du kan lägga till på några var klausuler för att ange dina finders ännu mer. Nedan ser vi bara på uppdrag som utförs av James Bond och bara de agenter som tillhör uppdraget Moonraker i det andra exemplet.

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

SQL

VÄLJ "uppdrag". * FRÅN "uppdrag" INNER JOIN "agenter" på "agenter". "Mission_id" = "uppdrag". "Id" VAR "agenter". "Namn" =? [["namn", "James Bond"]]

rails

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

SQL

VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id" WHERE "uppdrag". "Mission_name" =? [["mission_name", "Moonraker"]]

Med förenar, du måste också vara uppmärksam på singular och plural användning av dina modellföreningar. Eftersom min Uppdrag klass har många: agenter, vi kan använda plural. Å andra sidan, för Ombud klass belong_to: mission, endast singularversionen fungerar utan att blåsa upp. Viktig liten detalj: var en del är enklare. Eftersom du söker efter flera rader i tabellen som uppfyller ett visst tillstånd, är pluralformen alltid meningsfull.

Scopes

Områden är ett praktiskt sätt att extrahera vanliga frågebehov till egna namngivna metoder. På så sätt är de lite lättare att passera och möjligen lättare att förstå om andra måste arbeta med din kod eller om du behöver återkomma till vissa frågor i framtiden. Du kan definiera dem för enstaka modeller men använda dem för sina föreningar också. 

Himlen är gränsen verkligen-förenar, innefattar, och var är alla rättvisa spel! Eftersom omfattningar återkommer också Active :: Relations objekt, du kan kedja dem och ringa andra scopes ovanpå dem utan tvekan. Att extrahera scopes och att kedja dem för mer komplexa frågor är mycket praktiskt och gör längre sådana mer lättlästa. Områden definieras via "stabby lambda" -syntaxen:

rails

klassuppdrag < ActiveRecord::Base has_many: agents scope :successful, -> where (mission_complete: true) avsluta Mission.successful
klassmäklare < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> var (womanizer: true) scope: gambler, -> var (gambler: true) slut # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

VÄLJ "agenter". * FRÅN "agenter" VAR "agenter". "Licence_to_kill" =? OCH "agenter". "Womanizer" =? OCH "agenter". "Gambler" =? [["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Som du kan se från exemplet ovan, är det mycket trevligare att hitta James Bond när du bara kan köra scopes tillsammans. På så sätt kan du blanda och matcha olika frågor och stanna DRY samtidigt. Om du behöver scopes via föreningar, står de till ditt förfogande:

Mission.last.agents.licenced_to_kill.womanizer.gambler
VÄLJ "uppdrag". * FRÅN "uppdrag" ORDER BY "missioner". "Id" DESC LIMIT 1 VÄLJ "agenter". * FRÅN "agenter" VAR "agenter". "Mission_id" =? OCH "agenter". "Licence_to_kill" =? OCH "agenter". "Womanizer" =? OCH "agenter". "Gambler" =? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"]]

Du kan också omdefiniera default_scope för när du tittar på något liknande Mission.all.

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

SQL

 VÄLJ "uppdrag". * FRÅN "uppdrag" VAR "uppdrag". "Status" =? [["status", "pågående"]]

aggregeringar

Det här avsnittet är inte så mycket avancerat när det gäller förståelsen, men du behöver dem oftare än inte i scenarier som kan betraktas som lite mer avancerade än din genomsnittliga finnliknande .Allt, .först, .find_by_id eller vad som helst. Filtrering baserat på grundläggande beräkningar är förmodligen något som nybörjare inte kommer i kontakt med direkt. Vad tittar vi på exakt här?

  • summa
  • räkna
  • minimum
  • maximal
  • medel

Lätt peasy, eller hur? Den snygga saken är att istället för att slingra igenom en återkommande samling objekt för att göra dessa beräkningar kan vi låta Active Record göra allt detta arbete för oss och returnera dessa resultat med frågorna - i en fråga helst. Trevligt, va?

  • räkna

rails

Mission.count # => 24

SQL

VÄLJ COUNT (*) FRÅN "uppdrag"
  • medel

rails

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

SQL

VÄLJ AVG ("agenter". "Number_of_gadgets") FRÅN "agenter"

Eftersom vi nu vet hur vi kan utnyttja förenar, vi kan ta det här ett steg längre och bara fråga om det genomsnittliga antal prylar som agenterna har på ett visst uppdrag.

rails

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

SQL

Välj " [["namn", "Moonraker"]]

Gruppering av dessa genomsnittliga antal prylar med uppdragsnamn blir trivialt vid den tidpunkten. Se mer om att gruppera nedan:

rails

Agent.joins (: Mission) .group (missions.name) genomsnittet. (: Number_of_gadgets)

SQL

VÄLJ AVG ("agenter". "Number_of_gadgets") AS genomsnitt_number_of_gadgets, missions.name AS missions_name FRÅN "agenter" INNER JOIN "missions" PÅ "uppdrag". "Id" = "agenter". "Mission_id" GRUPP AV missions.name
  • summa

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 ("agenter". "Number_of_gadgets") FRÅN "agenter" SELECT SUM ("agenter". "Number_of_gadgets") FRÅN "agenter" VAR "agenter". "Licence_to_kill" =? ["licence_to_kill", "t"]] SELECT SUM ("agenter". "number_of_gadgets") FRÅN "agenter" VAR ("agenter". "licence_to_kill"! =?) [["licence_to_kill", "t"]]
  • maximal

rails

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

SQL

SELECT MAX ("agenter". "Number_of_gadgets") FRÅN "agenter" SELECT MAX ("agenter". "Number_of_gadgets") FRÅN "agenter" VAR "agenter". "Licence_to_kill" =? [["licence_to_kill", "t"]]
  • minimum

rails

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

SQL

VÄLJ MIN ("agenter". "Iq") FRÅN "agenter" SELECT MIN ("agenter". "Iq") FRÅN "agenter" VAR "agenter". "Licence_to_kill" =? [["licence_to_kill", "t"]]

Uppmärksamhet!

Alla dessa aggregeringsmetoder låter dig inte kedja på andra saker - de är terminala. Ordern är viktig för att göra beräkningar. Vi får inte en Active :: Relation objekt tillbaka från dessa operationer, vilket gör att musiken stannar vid den tiden-vi får en hash eller siffror istället. Exemplen nedan fungerar inte:

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 ')

grupperade

Om du vill att beräkningarna är uppdelade och sorterade i logiska grupper, ska du använda a GRUPP klausul och gör inte detta i Ruby. Vad jag menar med det är att du bör undvika iterering över en grupp som producerar potentiellt massor av frågor.

rails

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

SQL

VÄLJ AVG ("agenter". "Number_of_gadgets") AS genomsnitt_number_of_gadgets, missions.name AS missions_name FRÅN "agenter" INNER JOIN "missions" PÅ "uppdrag". "Id" = "agenter". "Mission_id" GRUPP AV missions.name

I det här exemplet hittar du alla agenter som är grupperade till ett visst uppdrag och returnerar en hash med det beräknade genomsnittliga antalet gadgets som dess värden - i en enda fråga! Japp! Samma sak gäller också de andra beräkningarna. I det här fallet gör det verkligen bättre att låta SQL göra jobbet. Antalet frågor vi brinner för dessa aggregeringar är bara för viktigt.

Dynamiska Finders

För varje attribut på dina modeller, säg namn, e-postadressfavorite_gadget och så vidare med Active Record kan du använda mycket läsbara sökmetoder som skapas dynamiskt för dig. Låter kryptisk, jag vet, men det betyder inte något annat än find_by_id eller find_by_favorite_gadget. De find_by del är standard, och Active Record hänger bara på namnet på attributet för dig. Du kan till och med få lägga till en ! om du vill att den här sökaren ska göra ett fel om ingenting kan hittas. Den sjuka delen är, du kan till och med kedja dessa dynamiska sökmetoder tillsammans. Precis som det här:

rails

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

SQL

VÄLJ "agenter". * FRÅN "agenter" VAR "agenter". "Namn" =? LIMIT 1 [["namn", "James Bond"]] VÄLJ "agenter". * FRÅN "agenter" VAR "agenter". "Namn" =? OCH "agenter". "Licence_to_kill" =? LIMIT 1 [["namn", "James Bond"], ["licence_to_kill", "t"]]

Självklart kan du gå med det här, men jag tror att det förlorar sin charm och användbarhet om du går utöver två attribut:

rails

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

SQL

VÄLJ "agenter". * FRÅN "agenter" VAR "agenter". "Namn" =? OCH "agenter". "Licence_to_kill" =? OCH "agenter". "Womanizer" =? OCH "agenter". "Gambler" =? OCH "agenter". "Number_of_gadgets" =? GRÄNS 1 [["namn", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["gambler", "t"], ["number_of_gadgets", 3 ]]

I det här exemplet är det dock trevligt att se hur det fungerar under huven. Varje ny _och_ lägger till en SQL OCH operatören att logiskt knyta attributen tillsammans. Sammantaget är den främsta fördelen med dynamiska finders läsbarhet, med tanke på alltför många dynamiska attribut, förlorar den fördelen snabbt. Jag använder sällan detta, kanske mestadels när jag spelar runt i konsolen, men det är definitivt bra att veta att Rails erbjuder denna snygga lura.

Specifika fält

Active Record ger dig möjlighet att returnera objekt som är lite mer fokuserade på attributen som de bär. Vanligtvis, om inte annat anges, kommer frågan att be om alla fält i rad via * (VÄLJ "agenter". *), och sedan bygger Active Record Ruby-objekt med den fullständiga uppsättningen attribut. Men du kan Välj bara specifika fält som ska returneras av frågan och begränsa antalet attribut som dina Ruby-objekt behöver "bära runt".

rails

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

SQL

VÄLJ "agenter". "Namn" FRÅN "agenter"

rails

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

SQL

VÄLJ "agenter". "Nummer", "agenter". "Favorite_gadget" FRÅN "agenter"

Som du kan se kommer de återkomna objekten bara att ha de valda attributen, plus deras ids naturligtvis-det är en given med något objekt. Det spelar ingen roll om du använder strängar, som ovan, eller symboler - frågan blir densamma.

rails

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

Ett försiktighetsåtgärd: Om du försöker komma åt attribut på det objekt som du inte har valt i dina frågor får du en MissingAttributeError. Sedan id kommer automatiskt att ges åt dig, men du kan fråga om id utan att välja det.

Anpassad SQL

Sist men inte minst kan du skriva din egen anpassade SQL via find_by_sql. Om du är övertygad nog i din egen SQL-Fu och behöver några anpassade samtal till databasen, kan den här metoden vara mycket praktisk ibland. Men det här är en annan historia. Glöm inte att leta efter Active Record wrapper-metoder först och undvik att återuppfinna hjulet där Rails försöker möta dig mer än halvvägs.

rails

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("VÄLJ namn, licence_to_kill FROM agents") 

Inte överraskande resulterar detta i:

SQL

VÄLJ * FRÅN AGENTER VÄLJ namn, licence_to_kill FROM agenter

Eftersom omfattningar och egna klassmetoder kan användas omväxlande för dina anpassade sökarfunktioner, kan vi ta ett steg längre för mer komplexa SQL-frågor. 

rails

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

Vi kan skriva klassmetoder som inkapslar SQL i ett här dokument. Detta låter oss skriva flera linjesträngar på ett mycket läsligt sätt och lagra sedan den SQL-strängen inuti en variabel som vi kan återanvända och övergå till find_by_sql. På det sättet gipsar vi inte massor av frågekod inom metallsamtalet. Om du har mer än ett ställe för att använda denna fråga, är det också DRY.

Eftersom det här är tänkt att vara nybörjevänlig och inte en SQL-handledning i sig, höll jag exemplet väldigt minimalistiskt av en anledning. Tekniken för långt mer komplexa frågor är dock ganska densamma. Det är lätt att föreställa sig att ha en anpassad SQL-fråga där ute som sträcker sig över tio kodkod. 

Gå som nötter som du behöver - rimligen! Det kan vara en livsparare. Ett ord om syntaxen här. De SQL del är bara en identifierare här för att markera början och slutet av strängen. Jag slår vad om att du inte behöver den här metoden så mycket - låt oss hoppas! Det har definitivt sin plats, och Rails land skulle inte vara detsamma utan det - i sällsynta fall att du absolut vill finjustera din egen SQL med den.

Slutgiltiga tankar

Jag hoppas att du fick lite mer bekväma skrivfrågor och läser den fruktade ol 'raw SQL. De flesta av de ämnen som vi behandlade i den här artikeln är viktiga för att skriva frågor som behandlar mer komplicerad affärslogik. Ta dig tid att förstå dessa och spela lite med frågor i konsolen. 

Jag är ganska säker på att när du lämnar handledning land bakom, kommer din Rails-kredit förr eller senare att öka väsentligt om du arbetar med dina första verkliga projekt och behöver skapa egna egna frågor. Om du fortfarande är lite blyg av ämnet, skulle jag säga att du bara har kul med det - det är verkligen ingen raketvetenskap!