In dit laatste deel gaan we wat dieper in vragen zoeken en spelen met een paar meer geavanceerde scenario's. We zullen in dit artikel de relaties van Active Record-modellen een beetje meer behandelen, maar ik blijf uit de buurt van voorbeelden die te verwarrend kunnen zijn voor het programmeren van nieuwkomers. Voordat u verder gaat, hoeft u geen verwarring te zaaien met dingen als het onderstaande voorbeeld:
Mission.last.agents.where (naam: 'James Bond')
Als u nog geen ervaring hebt met Active Record-query's en SQL, raad ik u aan mijn vorige twee artikelen te bekijken voordat u doorgaat. Deze kan misschien moeilijk te slikken zijn zonder de wetenschap die ik tot nu toe aan het opbouwen was. Tot jou, natuurlijk. Aan de andere kant, dit artikel zal niet zo lang zijn als de andere als je alleen maar naar deze iets geavanceerdere use-cases wilt kijken. Laten we ingaan!
Laten we herhalen. We kunnen meteen Active Record-modellen opvragen, maar associaties zijn ook een goed spel voor zoekopdrachten - en we kunnen al deze dingetjes aan elkaar koppelen. Tot zover goed. We kunnen zoekers ook in uw modellen verpakken in nette, herbruikbare scopes, en ik heb kort hun overeenkomst met klassemethoden genoemd.
class Agent < ActiveRecord::Base belongs_to :mission scope :find_bond, -> where (naam: 'James Bond') scope: licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womaniser: true) scope: gambler, -> where (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
Je kunt ze dus ook in je eigen klassemethoden verpakken en ermee klaar zijn. Scopes zijn niet dubieus of zoiets, denk ik - hoewel mensen ze hier en daar een beetje magie noemen - maar omdat klassemethoden hetzelfde bereiken, zou ik daarvoor kiezen.
class 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
Deze klassemethodes lezen precies hetzelfde en je hoeft niemand met een lambda neer te steken. Wat het beste werkt voor u of uw team; het is aan u welke API u wilt gebruiken. Mix en match ze gewoon niet - blijf bij één keuze! Met beide versies kunt u deze methoden gemakkelijk in een andere klassemethode onderbrengen, bijvoorbeeld:
class Agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womanizer: true) 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
class 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
Laten we een stap verder gaan, blijf bij mij. We kunnen een lambda in verenigingen zelf gebruiken om een bepaald bereik te definiëren. Het ziet er eerst een beetje raar uit, maar ze kunnen best handig zijn. Dat maakt het mogelijk om deze lambda's rechtstreeks bij uw verenigingen te noemen.
Dit is best gaaf en goed leesbaar met kortere methoden om te ketenen. Pas echter op dat deze modellen te strak worden gekoppeld.
klasse Missie < ActiveRecord::Base has_many :double_o_agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.double_o_agents
Vertel me dat dit op de een of andere manier niet cool is! Het is niet voor dagelijks gebruik, maar verdomd logisch. Dus hier Missie
kan alleen "agenten" aanvragen die de licentie hebben om te doden.
Een woord over de syntaxis, omdat we afdwaalden van naamgevingsconventies en iets expressievers gebruikten double_o_agents
. We moeten de klassenaam vermelden om Rails niet te verwarren, die anders misschien een klasse zou verwachten DoubleOAgent
. Je kunt beide natuurlijk hebben Middel
associaties op hun plaats - de gebruikelijke en jouw maatstaf - en Rails zullen niet klagen.
klasse Missie < ActiveRecord::Base has_many :agents has_many :double_o__agents, -> where (licence_to_kill: true), class_name: "Agent" end # => Mission.agents # => Mission.double_o_agents
Wanneer u de database doorzoekt voor records en u niet alle gegevens nodig heeft, kunt u aangeven wat u precies wilt retourneren. Waarom? Omdat de gegevens die worden teruggestuurd naar Active Record uiteindelijk worden ingebouwd in nieuwe Ruby Objects. Laten we naar één eenvoudige strategie kijken om geheugenzwakte in uw Rails-app te voorkomen:
klasse Missie < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end
Agent.all.joins (: mission)
SELECTEER "agents". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id"
Dus deze query retourneert een lijst met agents met een missie van de database naar Active Record - die vervolgens op zijn beurt gaat bouwen om er Ruby-objecten van te maken. De missie
gegevens zijn beschikbaar omdat de gegevens van deze rijen worden samengevoegd in de rijen met de gegevens van de agent. Dat betekent dat de gekoppelde gegevens beschikbaar zijn tijdens de query, maar niet worden teruggestuurd naar Actieve record. Dus je hebt deze gegevens om bijvoorbeeld berekeningen uit te voeren.
Het is vooral cool omdat je gegevens kunt gebruiken die niet ook teruggestuurd worden naar je app. Minder attributen die moeten worden ingebouwd in Ruby-objecten - die het geheugen opnemen - kunnen een grote overwinning zijn. Over het algemeen denk erover om alleen de absoluut noodzakelijke rijen en kolommen terug te sturen die je nodig hebt. Op die manier kun je bloat behoorlijk voorkomen.
Agent.all.joins (: missie) .waar (missies: doel: "De wereld redden")
Even een korte terloopse opmerking over de syntaxis hier: omdat we geen vragen stellen over de Middel
tafel via waar
, maar de verbonden :missie
tabel, moeten we specificeren dat we specifiek zoeken missies
in onze WAAR
clausule.
SELECTEER "agents". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id" WAAR "missies". "Objectief" =? [["doelstelling", "De wereld redden"]]
Gebruik makend van omvat
hier zouden ook missies naar Active Record worden geretourneerd voor gretig laden en geheugenruimte innemen met het bouwen van Ruby-objecten.
EEN samensmelten
is handig, bijvoorbeeld wanneer we een query willen combineren op agents en de bijbehorende missies met een specifieke scope die door u is gedefinieerd. We kunnen er twee nemen ActiveRecord :: Relation
objecten en voeg hun voorwaarden samen. Natuurlijk, geen buistelevisie, maar samensmelten
is handig als u een bepaalde scope wilt gebruiken terwijl u een associatie gebruikt.
Met andere woorden, wat we kunnen doen samensmelten
is filteren op een benoemd bereik op het gekoppelde model. In een van de vorige voorbeelden hebben we klassemethoden gebruikt om dergelijke benoemde bereiken zelf te definiëren.
klasse Missie < 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)
SELECTEER "agenten". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id" WAAR "missies". "Vijand" =? [["vijand", "Ernst Stavro Blofeld"]]
Wanneer we inkapselen wat een gevaarlijk
missie is binnen de Missie
model, we kunnen het op a zetten toetreden
via samensmelten
op deze manier. Dus het verplaatsen van de logica van dergelijke condities naar het relevante model waar het thuishoort, is aan de ene kant een leuke techniek om lossere koppelingen te bereiken - we willen niet dat onze Active Record-modellen veel details over elkaar kennen - en aan de andere kant hand geeft het je een mooie API in je joins zonder in je gezicht op te blazen. Het onderstaande voorbeeld zonder samenvoegen zou niet werken zonder een fout:
Agent.all.merge (Mission.dangerous)
SELECTEER "agenten". * VAN "agenten" WAAR "missies". "Vijand" =? [["vijand", "Ernst Stavro Blofeld"]]
Wanneer we nu een samenvoegen ActiveRecord :: Relation
object voor onze missies op onze agenten, de database weet niet over welke missies we het hebben. We moeten eerst duidelijk maken welke associatie we nodig hebben en eerst lid worden van de missiegegevens, anders raakt SQL in de war. Nog een kers op de taart. We kunnen dit nog beter inkapselen door ook de agenten te betrekken:
klasse Missie < 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
SELECTEER "agenten". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id" WAAR "missies". "Vijand" =? [["vijand", "Ernst Stavro Blofeld"]]
Dat is wat zoete kers in mijn boek. Inkapseling, goede OOP en uitstekende leesbaarheid. pot!
Hierboven hebben we de hoort bij
vereniging in actie veel. Laten we dit vanuit een ander perspectief bekijken en geheime servicecategorieën in de mix brengen:
klasse Sectie < 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
In dit scenario zouden agents dus niet alleen een mission_id
maar ook een SECTION_ID
. Tot zover goed. Laten we alle secties vinden met agenten met een specifieke missie - dus secties die een soort van missie hebben.
Section.joins (: middelen)
SELECTEER "secties". * FROM "secties" BINNEN JOIN "agenten" AAN "agenten". "Sectie_id" = "secties." Id "
Heb je iets opgemerkt? Een klein detail is anders. De externe sleutels worden omgedraaid. Hier vragen we om een lijst met secties, maar gebruik externe sleutels zoals deze: "agents". "section_id" = "secties." id "
. Met andere woorden, we zijn op zoek naar een externe sleutel van een tafel waaraan we meedoen.
Agent.joins (: mission)
SELECTEER "agents". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id"
Eerder onze joins via een hoort bij
associatie zag er als volgt uit: de externe sleutels werden gespiegeld ("missies". "id" = "agents". "mission_id"
) en waren op zoek naar de buitenlandse sleutel van de tafel die we beginnen.
Teruggaan naar uw heeft veel
scenario, we zouden nu een lijst met secties krijgen die worden herhaald, omdat ze natuurlijk meerdere agents in elke sectie hebben. Dus voor elke agentkolom die wordt samengevoegd, krijgen we een rij voor deze sectie of section_id-in het kort, we dupliceren in feite rijen. Om dit nog meer duizelingwekkend te maken, laten we ook missies in de mix brengen.
Section.joins (agents:: mission)
SELECTEER "secties". * FROM "secties" INNER JOIN "agenten" AAN "agenten". "Section_id" = "secties". "Id" INNER JOIN "missies" AAN "missies". "Id" = "agents". " mission_id"
Bekijk de twee BINNEN DOEN
onderdelen. Nog steeds bij me? We "bereiken" via agenten naar hun missies vanuit het gedeelte van de agent. Ja, dingen voor leuke hoofdpijn, ik weet het. Wat we krijgen zijn missies die indirect associëren met een bepaald deel.
Als gevolg hiervan krijgen we nieuwe kolommen toegevoegd, maar het aantal rijen is nog steeds hetzelfde dat door deze query wordt geretourneerd. Wat wordt teruggestuurd naar Active Record, wat resulteert in het bouwen van nieuwe Ruby-objecten, is ook nog steeds de lijst met secties. Dus wanneer we meerdere missies met meerdere agenten uitvoeren, krijgen we opnieuw gedupliceerde rijen voor onze sectie. Laten we dit nog eens filteren:
Section.joins (agents:: mission) .where (missies: vijand: "Ernst Stavro Blofeld")
SELECTEER "secties". * FROM "secties" INNER JOIN "agenten" AAN "agenten". "Section_id" = "secties". "Id" INNER JOIN "missies" AAN "missies". "Id" = "agents". " mission_id "WHERE" missions "." enemy "= 'Ernst Stavro Blofeld'
Nu krijgen we alleen secties terug die betrokken zijn bij missies waar Ernst Stavro Blofeld de betrokken vijand is. Cosmopolitan, zoals sommige superschurken misschien aan zichzelf denken, ze zouden in meer dan één sectie kunnen opereren - zeg bijvoorbeeld sectie A en C, de Verenigde Staten en Canada.
Als we meerdere agents in een bepaalde sectie hebben die aan dezelfde missie werken om Blofeld of wat dan ook te stoppen, zouden we opnieuw herhaalde rijen aan ons teruggeven in Active Record. Laten we er wat duidelijker over zijn:
Section.joins (agents:: mission) .where (missions: enemy: "Ernst Stavro Blofeld").
SELECTEER VERSCHILLENDE "secties". * FROM "secties" INNER JOIN "agenten" AAN "agenten". "Section_id" = "secties". "Id" INNER JOIN "missies" AAN "missies". "Id" = "agenten". "mission_id" WHERE "missions". "enemy" = 'Ernst Stavro Blofeld'
Wat dit ons geeft is het aantal secties waar Blofeld vanuit opereert - dat is bekend - dat agenten heeft die actief zijn in missies met hem als een vijand. Als laatste stap, laten we opnieuw refactoring doen. We extraheren dit in een leuke "kleine" klassemethode klasse Sectie
:
klasse Sectie < 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
Je kunt dit nog meer refacteren en de verantwoordelijkheden opsplitsen om een lossere koppeling tot stand te brengen, maar laten we verdergaan.
Meestal kunt u erop vertrouwen dat Active Record de SQL schrijft die u voor hem wilt. Dat betekent dat je in Ruby land blijft en je geen zorgen hoeft te maken over databasedetails. Maar soms moet je een gat in SQL-land steken en je eigen ding doen. Als u bijvoorbeeld een LINKS
Doe mee en doorbreek het gebruikelijke gedrag van Active Record van het doen van een INNER
join standaard. doet mee
is een klein venster om indien nodig uw eigen aangepaste SQL te schrijven. U opent het, sluit uw aangepaste querycode aan, sluit het "venster" en kunt blijven toevoegen aan Active Record-opvraagmethoden.
Laten we dit demonstreren met een voorbeeld met gadgets. Laten we zeggen een typische agent meestal heeft veel
gadgets, en we willen agenten vinden die niet over de nodige gadgets beschikken om hen in het veld te helpen. Een gebruikelijke join zou geen goede resultaten opleveren, omdat we er echt in geïnteresseerd zijn nul
-of nul
in SQL-spreekwaarden van deze speelgoedenspionnen.
klasse Missie < 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
Wanneer we een doen doet mee
operatie, krijgen we alleen agenten geretourneerd die al zijn uitgerust met gadgets, omdat de agent_id
op deze gadgets is niet nul. Dit is het verwachte gedrag van een standaard inner join. De interne join bouwt voort op een overeenkomst aan beide kanten en retourneert alleen rijen gegevens die overeenkomen met deze voorwaarde. Een niet-bestaande gadget met een nul
waarde voor een agent die geen gadget draagt, komt niet overeen met dat criterium.
Agent.joins (: gadgets)
SELECTEER "agents". * FROM "agents" INNER JOIN "gadgets" AAN "gadgets". "Agent_id" = "agents". "Id"
We zijn aan de andere kant op zoek naar schmuckagenten die dringend behoefte hebben aan wat liefde van de kwartiermeester. Uw eerste schatting kan er als volgt uitzien:
Agent.joins (: gadgets) .where (gadgets: agent_id: nil)
SELECTEER "agents". * FROM "agents" INNER JOIN "gadgets" AAN "gadgets". "Agent_id" = "agents". "Id" WAAR "gadgets". "Agent_id" IS NULL
Niet slecht, maar zoals je kunt zien aan de SQL-uitvoer, speelt het niet mee en staat het nog steeds op de standaardwaarde BINNEN DOEN
. Dat is een scenario waar we een nodig hebben BUITENSTE
meedoen, omdat de ene kant van onze "vergelijking" ontbreekt, om zo te zeggen. We zijn op zoek naar resultaten voor gadgets die niet bestaan, meer bepaald voor agenten zonder gadgets.
Tot nu toe, toen we een symbool voor een actief record in een join hebben doorgegeven, verwachtte het een associatie. Als een string wordt doorgegeven, verwacht hij dat het een echt fragment van SQL-code is, een deel van uw zoekopdracht.
Agent.joins ("LEFT OUTER JOIN-gadgets op gadgets.agent_id = agents.id"). Where (gadgets: agent_id: nil)
SELECTEER "agenten". * FROM "agenten" LINKEROUTER JOIN-gadgets OP gadgets.agent_id = agents.id WAAR "gadgets". "Agent_id" IS NULL
Of, als je nieuwsgierig bent naar luie agenten zonder missies - misschien hangend in Barbados of waar dan ook - zou onze aangepaste join er als volgt uitzien:
Agent.joins ("LEFT OUTER JOIN JOIN ON missions.id = agents.mission_id"). Where (missions: id: nil)
SELECTEER "agenten". * FROM "agents" LINKER BUITEN BINNEN ZENDING missies ON missions.id = agents.mission_id WAAR "missies". "Id" IS NULL
De outer join is de meer inclusieve join-versie omdat deze overeenkomt met alle records uit de gekoppelde tabellen, zelfs als sommige van deze relaties nog niet bestaan. Omdat deze aanpak niet zo exclusief is als inner joins, krijg je hier en daar een hoop nils. Dit kan in sommige gevallen informatief zijn, maar innerlijke joins zijn toch meestal waar we naar op zoek zijn. Rails 5 laat ons een gespecialiseerde methode gebruiken genaamd left_outer_joins
in plaats daarvan voor dergelijke gevallen. Tenslotte!
Een kleinigheid voor de weg: houd deze kijkgaten in SQL-land zo klein mogelijk als je kunt. Je zult iedereen - inclusief je toekomstige zelf - een enorme gunst doen.
Actief zijn Record om efficiënt SQL voor je te schrijven is een van de belangrijkste vaardigheden die je moet afnemen van deze miniserie voor beginners. Op die manier krijgt u ook code die compatibel is met de database die wordt ondersteund, wat betekent dat de query's in verschillende databases stabiel zijn. Het is noodzakelijk dat u niet alleen begrijpt hoe u met Active Record moet spelen, maar ook de onderliggende SQL, die even belangrijk is.
Ja, SQL kan saai zijn, vervelend om te lezen en niet elegant uitzien, maar vergeet niet dat Rails Active Record rond SQL omwikkelt en je moet het begrijpen van dit essentiële stukje technologie niet verwaarlozen, alleen omdat Rails het heel gemakkelijk maakt om niet de meeste zorgen te maken van de tijd. Efficiëntie is cruciaal voor databasequery's, vooral als u iets maakt voor een groter publiek met veel verkeer.
Ga nu op de internets en vind wat meer materiaal over SQL om het uit je systeem te krijgen - voor eens en voor altijd!