I det här sista stycket ska vi titta lite djupare på frågor och spela med några mer avancerade scenarier. Vi kommer att täcka relationer med Active Record-modellerna lite mer i den här artikeln, men jag kommer att hålla mig borta från exempel som kan vara förvirrande för programmering av nybörjare. Innan du går framåt, bör saker som exemplet nedan inte orsaka någon förvirring:
Mission.last.agents.where (namn: "James Bond")
Om du är ny på Active Record-frågor och SQL rekommenderar jag att du tar en titt på mina tidigare två artiklar innan du fortsätter. Den här kan vara svår att svälja utan att jag har byggt upp hittills. Upp till dig självklart. På flipsidan kommer den här artikeln inte vara så lång som de andra om du bara vill titta på dessa lite avancerade användarfall. Låt oss gräva in!
Låt oss upprepa. Vi kan fråga Active Record-modeller direkt, men föreningar är också rättvisa spel för frågor - och vi kan kedja alla dessa saker. Än så länge är allt bra. Vi kan också packa finders i snygga, återanvändbara omfång i dina modeller, och jag nämnde kort deras likhet med klassmetoder.
klassmäklare < ActiveRecord::Base belongs_to :mission scope :find_bond, -> where (name: 'James Bond') scope: licensed_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) scope: gambler 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
Så du kan också packa dem i dina egna klassmetoder och göra det med det. Omfattningar är inte iffy eller någonting, jag tror att även om folk nämner dem som lite magiska här och där-men eftersom klassmetoder uppnår samma sak skulle jag välja det.
klassmäklare < 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
Dessa klassmetoder läser precis samma sak, och du behöver inte knäcka någon med en lambda. Vad som helst bäst för dig eller ditt lag Det är upp till dig vilket API du vill använda. Bara blanda inte och matcha dem - hålla med ett val! Båda versionerna låter dig enkelt kedja dessa metoder i en annan klassmetod, till exempel:
klassmäklare < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> var (womanizer: true) def self.find_licenced_to_kill_womanizer womanizer.licenced_to_kill slutet slutet = = Agent.find_licenced_to_kill_womanizer # => Mission.last.agents.find_licenced_to_kill_womanizer
klassmäklare < 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
Låt oss ta detta lilla steg längre - stanna hos mig. Vi kan använda en lambda i föreningarna själva för att definiera ett visst räckvidd. Det ser lite konstigt ut först, men de kan vara ganska praktiska. Det gör det möjligt att ringa dessa lambdas direkt på dina föreningar.
Detta är ganska coolt och mycket läsbart med kortare metoder kedjning pågår. Akta dig för att koppla dessa modeller för täta, dock.
klassuppdrag < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents
Berätta för mig det här är inte häftigt på något sätt! Det är inte för vardagligt bruk, men dop att jag antar. Så här Uppdrag
kan "begära" endast agenter som har licensen att döda.
Ett ord om syntaxen, eftersom vi avled från namngivna konventioner och använde något mer uttrycksfullt som double_o_agents
. Vi behöver nämna klassnamnet för att inte förväxla Rails, som annars skulle kunna vänta sig att leta efter en klass DoubleOAgent
. Du kan självklart ha båda Ombud
föreningar på plats-det vanliga och din anpassade en och Rails kommer inte att klaga.
klassuppdrag < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (licence_to_kill: true), class_name: "Agent" slut # => Mission.agents # => Mission.double_o_agents
När du frågar databasen för poster och du inte behöver all data, kan du välja att ange vad exakt du vill ha tillbaka. Varför? Eftersom data som returneras till Active Record så småningom kommer att byggas in i nya Ruby Objects. Låt oss titta på en enkel strategi för att undvika minnesblåsningar i din Rails-app:
klassuppdrag < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.all.joins (: Mission)
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id"
Så denna fråga returnerar en lista över agenter med ett uppdrag från databasen till Active Record-som i sin tur anger att bygga Ruby-objekt ut ur det. De uppdrag
data är tillgänglig eftersom data från dessa rader är förenade med raden av agenten data. Det betyder att den sammanslagna data är tillgänglig under frågan men kommer inte att returneras till Active Record. Så du kommer att få denna data att utföra beräkningar, till exempel.
Det är särskilt coolt eftersom du kan använda data som inte heller skickas tillbaka till din app. Färre attribut som måste byggas in i Ruby-objekt - som tar upp minne - kan vara en stor vinst. I allmänhet, tänk på att skicka bara de absoluta nödvändiga raderna och kolumnerna som du behöver. På det sättet kan du undvika uppblåsa lite.
Agent.all.joins (: mission) .where (uppdrag: mål: "Saving the World")
Bara en snabb sida om syntaxen här: för att vi inte frågar Ombud
bord via var
, men den förenade :uppdrag
tabell måste vi ange att vi letar efter specifika beskickningar
i vår VAR
klausul.
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id" WHERE "uppdrag". [["mål", "Saving the World"]]
Använder sig av innefattar
här skulle också returnera uppdrag till Active Record för ivrig lastning och ta upp minnesbyggande Ruby-objekt.
en sammanfoga
kommer till nytta, till exempel när vi vill kombinera en fråga om agenter och deras tillhörande uppdrag som har ett specifikt räckvidd som definieras av dig. Vi kan ta två Active :: Relation
objekt och slå samman deras förhållanden. Visst, ingen biggie, men sammanfoga
är användbart om du vill använda en viss räckvidd när du använder en förening.
Med andra ord, vad vi kan göra med sammanfoga
är filter med ett namngivne räckvidd på den anslutna modellen. I ett av de föregående exemplen använde vi klassmetoder för att definiera sådana namnlistor vi själva.
klassuppdrag < 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 (: mission) .merge (Mission.dangerous)
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id" VAR "missioner". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
När vi inkapslar vad a farlig
uppdraget är inom Uppdrag
modell, vi kan stoppa den på en Ansluta sig
via sammanfoga
den här vägen. Att flytta logiken över sådana förhållanden till den relevanta modellen där den tillhör är på den ena sidan en bra teknik för att uppnå en slätare koppling. Vi vill inte att våra Active Record-modeller ska veta mycket om varandra - och å andra sidan hand, det ger dig ett trevligt API i dina sammanfogningar utan att blåsa upp i ditt ansikte. Exemplet nedan utan att slå samman skulle inte fungera utan ett fel:
Agent.all.merge (Mission.dangerous)
VÄLJ "agenter". * FRÅN "agenter" VAR "uppdrag". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
När vi nu slår samman en Active :: Relation
objekt för våra uppdrag på våra agenter, databasen vet inte vilka uppdrag vi pratar om. Vi måste klargöra vilken förening vi behöver och ansluta oss till uppdragsuppgifterna först eller SQL blir förvirrad. En sista körsbär på toppen. Vi kan inkapsla detta ännu bättre genom att involvera agenterna också:
klassuppdrag < 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
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id" VAR "missioner". "Fiende" =? [["fiende", "Ernst Stavro Blofeld"]]
Det är lite söt körsbär i min bok. Inkapsling, korrekt OOP och stor läsbarhet. Jackpott!
Ovan har vi sett tillhör
föreningen i åtgärd mycket. Låt oss ta en titt på detta från ett annat perspektiv och ta hem hemliga serviceavsnitt i mixen:
klass avsnitt < 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 detta scenario skulle agenter inte bara ha en mission_id
men också a SECTION_ID
. Än så länge är allt bra. Låt oss hitta alla sektioner med agenter med ett visst uppdrag, så sektioner som har någon form av uppdrag pågår.
Section.joins (: medel)
VÄLJ "sektioner". * FRÅN "avsnitt" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections." Id "
Såg du något? En liten detalj är annorlunda. De främmande nycklarna vänds. Här begär vi en lista med avsnitt men använder främmande nycklar som denna: "agenter". "section_id" = "sections." id "
. Med andra ord letar vi efter en främmande nyckel från ett bord som vi går med.
Agent.joins (: Mission)
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "Mission_id"
Tidigare ansluter vi oss via a tillhör
föreningen såg ut så här: de främmande nycklarna speglades ("uppdrag". "id" = "agenter". "mission_id"
) och letade efter den främmande nyckeln från bordet vi börjar.
Kommer tillbaka till din har många
scenario skulle vi nu få en lista över avsnitt som upprepas eftersom de har flera agenter i varje avsnitt, förstås. Så för varje agent kolumn som går samman, får vi en rad för det här avsnittet eller section_id-in short, vi duplicerar rader i grunden. För att göra detta ännu mer yrande, låt oss ta med uppdrag i blandningen också.
Section.joins (agenter:: mission)
VÄLJ "sektioner". * FRÅN "avsnitt" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter" mission_id"
Kolla in de två INRE KOPPLING
delar. Fortfarande med mig? Vi är "nå" genom agenter till sina uppdrag från agenten. Ja, grejer för roliga huvudvärk, jag vet. Vad vi får är uppdrag som indirekt associerar med en viss sektion.
Som ett resultat får vi nya kolumner, men antalet rader är fortfarande detsamma som returneras av den här frågan. Vad som skickas tillbaka till Active Record - vilket resulterar i att bygga nya Ruby-objekt - är också fortfarande listan över avsnitt. Så när vi har flera uppdrag pågår med flera agenter, skulle vi få dubbla rader för vårt avsnitt igen. Låt oss filtrera detta lite mer:
Section.joins (agenter:: mission) .where (uppdrag: fiende: "Ernst Stavro Blofeld")
VÄLJ "sektioner". * FRÅN "avsnitt" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter" mission_id "WHERE" uppdrag "." fiende "=" Ernst Stavro Blofeld "
Nu får vi bara avdelningar som är inblandade i uppdrag där Ernst Stavro Blofeld är den inblandade fienden. Cosmopolitan som vissa super skurkar kan tänka på sig själva, skulle de kunna fungera i mer än en sektion, säger avsnitt A och C, Förenta staterna respektive Kanada.
Om vi har flera agenter i en viss sektion som arbetar med samma uppgift att stoppa Blofeld eller vad som helst, skulle vi återigen ha upprepade rader tillbaka till oss i Active Record. Låt oss vara lite mer tydliga om det:
Section.joins (agenter:: mission) .where (uppdrag: fiende: "Ernst Stavro Blofeld").
VÄLJ DISTINCT "sektioner". * FRÅN "avsnitt" INNER JOIN "agenter" ON "agenter". "Section_id" = "sections". "Id" INNER JOIN "missioner" på "uppdrag". "Id" = "agenter". "mission_id" WHERE "missioner". "fiende" = "Ernst Stavro Blofeld"
Vad detta ger oss är antalet sektioner som Blofeld driver ut ur - som är kända - som har agenter aktiva i uppdrag med honom som en fiende. Som ett sista steg, låt oss göra några refactoring igen. Vi extraherar detta till en fin "liten" klassmetod på klass avsnitt
:
klass avsnitt < 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 detta ännu mer och dela upp ansvaret för att uppnå lösare koppling, men låt oss gå vidare för nu.
För det mesta kan du lita på Active Record och skriva SQL som du vill ha för dig. Det betyder att du bor i Ruby land och behöver inte oroa dig för databasinformationen för mycket. Men ibland måste du kasta ett hål i SQL-land och göra din egen sak. Till exempel, om du behöver använda en VÄNSTER
gå med och bryta ut av Active Records vanliga beteende att göra en INRE
gå med som standard. förenar
är ett litet fönster för att skriva din egen anpassade SQL om det behövs. Du öppnar det, pluggar in din anpassade fråge kod, stänger "fönstret" och kan fortsätta att lägga till i aktiva sökordsmetoder.
Låt oss visa detta med ett exempel som involverar prylar. Låt oss säga ett typiskt medel vanligtvis har många
gadgets, och vi vill hitta agenter som inte är utrustade med några fina gadgetry att hjälpa dem i fältet. Ett vanligt deltagande skulle inte ge bra resultat eftersom vi faktiskt är intresserade av noll
-eller null
i SQL-talvärden för dessa spionleksaker.
klassuppdrag < 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 gör en förenar
operation får vi bara återvändande agenter som redan är utrustade med prylar eftersom agent_id
på dessa prylar är inte noll. Detta är det förväntade beteendet hos en standardinlägg. Den inre föreningen bygger på en match på båda sidor och returnerar bara rader av data som matchar detta tillstånd. En obefintlig gadget med a noll
värdet för en agent som inte har någon gadget överensstämmer inte med det kriteriet.
Agent.joins (: prylar)
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "gadgets" ON "prylar". "Agent_id" = "agenter". "Id"
Vi söker å andra sidan schmuckagenter som har ett brådskande behov av lite kärlek från kvartmästaren. Din första gissning kunde ha sett ut som följande:
Agent.joins (: gadgets) .where (gadgets: agent_id: nil)
VÄLJ "agenter". * FRÅN "agenter" INNER JOIN "gadgets" ON "prylar". "Agent_id" = "agenter". "Id" VAR "prylar". "Agent_id" ÄR NULL
Inte dåligt, men som du kan se från SQL-utmatningen, spelar det inte med sig och insisterar fortfarande på standardvärdet INRE KOPPLING
. Det är ett scenario där vi behöver en YTTRE
gå med, för att en sida av vår "ekvation" saknas, så att säga. Vi letar efter resultat för gadgets som inte existerar-mer exakt, för agenter utan prylar.
Hittills, när vi passerade en symbol till Active Record i ett deltagande, väntade vi en förening. Med en sträng som passerat in, å andra sidan förväntar den sig att det är ett verkligt fragment av SQL-kod - en del av din fråga.
Agent.joins ("VÄNSTER UTGÅENDE JOIN-gadgets PÅ gadgets.agent_id = agents.id"). Var (gadgets: agent_id: nil)
VÄLJ "agenter". * FRÅN "agenter" VÄNSTER UTGÅENDE JOIN-gadgets PÅ gadgets.agent_id = agents.id Var "gadgets". "Agent_id" ÄR NULL
Eller om du är nyfiken på lata agenter utan uppdrag-hängande eventuellt i Barbados eller var som helst - vårt anpassade deltagande skulle se ut så här:
Agent.joins ("VÄNSTER UTGÅENDE JOIN-uppdrag på missions.id = agents.mission_id"). Var (uppdrag: id: nil)
VÄLJ "agenter". * FRÅN "agenter" VÄNSTER UTGÅENDE JOIN-uppdrag PÅ missions.id = agents.mission_id VAR "uppdrag". "Id" är NULL
Den yttre anslutningen är den mer inkluderade anslutningsversionen eftersom den kommer att matcha alla poster från de sammanfogade tabellerna, även om några av dessa relationer inte existerar ännu. Eftersom detta tillvägagångssätt inte är lika exklusivt som inre föreningar, kommer du att få en massa nils här och där. Detta kan vara informativt i vissa fall, naturligtvis, men inre anknytningar är likväl vanligtvis vad vi letar efter. Rails 5 låter oss använda en specialiserad metod som heter left_outer_joins
istället för sådana fall. Till sist!
En liten sak för vägen: Håll dessa kikande hål i SQL-land så liten som möjligt om du kan. Du kommer att göra alla - inklusive din framtid själv - en enorm fördel.
Att få aktiv post för att skriva effektiv SQL för dig är en av de viktigaste färdigheterna som du borde ta bort från den här mini-serien för nybörjare. På det sättet får du också kod som överensstämmer med vilken databas den stöder, vilket betyder att frågorna kommer att vara stabila över databaser. Det är nödvändigt att du inte bara förstår hur man spelar med Active Record utan även den underliggande SQL som är lika viktig.
Ja, SQL kan vara tråkigt, tråkigt att läsa och inte elegant, men glöm inte att Rails wraps Active Record runt SQL och du bör inte försumma att förstå denna viktiga teknik - bara för att Rails gör det väldigt lätt att inte bry sig om de flesta av tiden. Effektivitet är avgörande för databasfrågor, speciellt om du bygger något för större publik med tung trafik.
Gå nu ut på internet och hitta lite mer material på SQL för att få ut det från ditt system - en gång för alla!