Query's in Rails, deel 3

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! 

Onderwerpen

  • Scopes & Associations
  • Slimmer sluit zich aan
  • samensmelten
  • heeft veel
  • Aangepaste joins

Scopes & Associations

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.

rails

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.

rails

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:

rails

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

rails

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.

rails

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.

rails

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

Slimmer sluit zich aan

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:

rails

klasse Missie < ActiveRecord::Base has_many :agents end class Agent < ActiveRecord::Base belongs_to :mission end

rails

Agent.all.joins (: mission)

SQL

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.

rails

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.

SQL

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.

samensmelten

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.

rails

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

rails

Agent.joins (: mission) .merge (Mission.dangerous)

SQL

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:

rails

Agent.all.merge (Mission.dangerous)

SQL

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: 

rails

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

rails

Agent.double_o_engagements

SQL

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!

heeft veel

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:

rails

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.

rails

Section.joins (: middelen)

SQL

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.

rails

Agent.joins (: mission)

SQL

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.

rails

Section.joins (agents:: mission)

SQL

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:

rails

Section.joins (agents:: mission) .where (missies: vijand: "Ernst Stavro Blofeld") 

SQL

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:

rails

Section.joins (agents:: mission) .where (missions: enemy: "Ernst Stavro Blofeld").

SQL

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:

rails

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.

Aangepaste joins 

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.

rails

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. 

rails

Agent.joins (: gadgets)

SQL

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:

rails

Agent.joins (: gadgets) .where (gadgets: agent_id: nil) 

SQL

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.

rails

Agent.joins ("LEFT OUTER JOIN-gadgets op gadgets.agent_id = agents.id"). Where (gadgets: agent_id: nil)

SQL

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:

rails

Agent.joins ("LEFT OUTER JOIN JOIN ON missions.id = agents.mission_id"). Where (missions: id: nil)

SQL

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.

Laatste gedachten

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!