Query's in Rails, deel 2

In dit tweede artikel duiken we een beetje dieper in Active Record-vragen in Rails. Als u nog niet vertrouwd bent met SQL, voeg ik voorbeelden toe die eenvoudig genoeg zijn om mee te taggen en de syntaxis een beetje op te pikken als we gaan. 

Dat gezegd hebbende, zou het zeker helpen als je een snelle SQL-tutorial doorloopt voordat je terugkomt om verder te lezen. Neem anders de tijd om de SQL-query's te begrijpen die we hebben gebruikt en ik hoop dat aan het einde van deze serie het niet meer intimiderend zal aanvoelen. 

Het meeste is echt eenvoudig, maar de syntaxis is een beetje raar als je net bent begonnen met coderen, vooral in Ruby. Wacht daar, het is geen rocket science!

Onderwerpen

  • Inclusief & Eager laden
  • Deelnemen aan tabellen
  • Eager laden
  • scopes
  • samenvoegingen
  • Dynamische zoekers
  • Specifieke velden
  • Aangepaste SQL

Inclusief & Eager laden

Deze query's bevatten meer dan één databasetabel om mee te werken en zijn misschien wel de belangrijkste om dit artikel weg te nemen. Het komt erop neer: in plaats van meerdere query's uit te voeren voor informatie die is verdeeld over meerdere tabellen, omvat probeert deze tot een minimum te beperken. Het belangrijkste concept hierachter is 'gretig laden' en betekent dat we bijbehorende objecten laden wanneer we een zoekopdracht uitvoeren.

Als we dat deden door een verzameling objecten te itereren en vervolgens proberen om de bijbehorende records van een andere tabel te benaderen, zouden we een probleem tegenkomen dat het "N + 1 query-probleem" wordt genoemd. Bijvoorbeeld voor elk agent.handler in een verzameling agents vuren we afzonderlijke query's af voor zowel agenten als hun handlers. Dat is wat we moeten vermijden, want dit schaalt helemaal niet. In plaats daarvan doen we het volgende:

rails

agents = Agent.includes (: handlers)

Als we nu een dergelijke verzameling agenten doorleefden, zonder in te houden dat we het aantal geretourneerde records voorlopig niet hebben beperkt, zullen we uiteindelijk twee zoekopdrachten uitvoeren in plaats van mogelijk een gazillion. 

SQL

SELECTEER "agents". * FROM "agents" SELECT "handlers". * FROM "handlers" WAAR "handlers". "Id" IN (1, 2)

Deze ene agent in de lijst heeft twee handlers en als we nu het agent-object vragen aan de handlers, hoeven er geen extra databasequery's te worden geactiveerd. We kunnen dit natuurlijk een stap verder zetten en graag meerdere gekoppelde tabelrecords laden. Als we niet alleen handlers, maar ook de geassocieerde missies van de agent om welke reden dan ook zouden moeten laden, zouden we kunnen gebruiken omvat zoals dit.

rails

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

Eenvoudig! Wees voorzichtig met het gebruik van enkelvoudige en meervoudige versies voor de inhoud. Ze zijn afhankelijk van uw modelassociaties. EEN heeft veel associatie gebruikt meervoud, terwijl a hoort bij of a heeft een heeft de enkelvoudige versie natuurlijk nodig. Als je dat wilt, kun je ook stoppen met a waar clausule voor het opgeven van aanvullende voorwaarden, maar de voorkeursmanier om voorwaarden voor bijbehorende tabellen te specificeren die gretig zijn geladen, is door te gebruiken doet mee in plaats daarvan. 

Eén ding om in gedachten te houden over enthousiast laden is dat de gegevens die worden toegevoegd, volledig worden teruggestuurd naar Active Record - die op zijn beurt Ruby-objecten met deze kenmerken bouwt. Dit staat in schril contrast met het "eenvoudigweg" meedoen met de gegevens, waarbij u een virtueel resultaat krijgt dat u bijvoorbeeld voor berekeningen kunt gebruiken en minder geheugenverlies zult hebben dan inclusief.

Deelnemen aan tabellen

Het samenvoegen van tabellen is een ander hulpmiddel waarmee u kunt voorkomen dat u teveel overbodige vragen door de pijplijn stuurt. Een veelvoorkomend scenario is het samenvoegen van twee tabellen met een enkele query die een soort gecombineerd record retourneert. doet mee is gewoon een andere vindermethode van Active Record waarmee je kunt werken - in SQL-termen-JOIN tafels. Deze query's kunnen records uit meerdere tabellen combineren en u krijgt een virtuele tabel die records uit deze tabellen combineert. Dit is behoorlijk rad als je dat vergelijkt met het afvuren van allerlei query's voor elke tabel. Er zijn een paar verschillende soorten gegevensoverlap die u kunt krijgen met deze aanpak. 

De inner join is de standaard modus operandi voor doet mee. Dit komt overeen met alle resultaten die overeenkomen met een bepaalde id en de weergave ervan als een externe sleutel van een ander object of een andere tabel. In het onderstaande voorbeeld, simpel gezegd: geef me alle missies waar de missie is ID kaart verschijnt als mission_id in de tabel van een agent. "agents". "mission_id" = "missies". "id". Inner joins sluiten relaties uit die niet bestaan.

rails

Mission.joins (: middelen)

SQL

SELECTEER "missies". * FROM "missions" BINNEN JOIN "agents" AAN "agents". "Mission_id" = "missie". "Id"

Dus we matchen missies en hun begeleidende agenten in één enkele vraag! Natuurlijk, we zouden de missies eerst kunnen krijgen, ze één voor één kunnen herhalen en om hun agenten kunnen vragen. Maar dan gaan we terug naar ons vreselijke "N + 1-vraagstuk". Nee, dank u! 

Wat ook leuk is aan deze aanpak, is dat we geen enkele case krijgen met inner joins; we krijgen alleen geretourneerde records die overeenkomen met hun id's met externe sleutels in gekoppelde tabellen. Als we bijvoorbeeld missies moeten vinden die geen agenten bevatten, hebben we in plaats daarvan een outer join nodig. Omdat dit momenteel het schrijven van je eigen inhoudt OUTER JOIN SQL, we zullen dit in het laatste artikel bekijken. Terug naar standaard joins, u kunt natuurlijk ook deelnemen aan meerdere bijbehorende tabellen.

rails

Mission.joins (: agents,: expenses,: handlers)

En daar kun je wat aan toevoegen waar om uw vinders nog meer te specificeren. Hieronder kijken we alleen naar missies die worden uitgevoerd door James Bond en alleen de agenten die behoren tot de missie 'Moonraker' in het tweede voorbeeld.

Mission.joins (: agents) .where (agents: name: 'James Bond')

SQL

SELECTEER "missies" * FROM "missions" BINNEN JOIN "agents" AAN "agents". "Mission_id" = "missies". "Id" WAAR "agents". "Naam" =? [["naam", "James Bond"]]

rails

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

SQL

SELECTEER "agents". * FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id" WHERE "missions". "Mission_name" =? [["mission_name", "Moonraker"]]

Met doet mee, je moet ook aandacht besteden aan enkelvoud en meervoudig gebruik van je modelassociaties. Omdat mijn Missie klasse has_many: agents, we kunnen het meervoud gebruiken. Aan de andere kant, voor de Middel klasse behoort_naar: missie, alleen de enkelvoudige versie werkt zonder op te blazen. Belangrijke kleine details: de waar deel is eenvoudiger. Aangezien u naar meerdere rijen in de tabel zoekt die aan een bepaalde voorwaarde voldoen, is de meervoudsvorm altijd zinvol.

scopes

Scopes zijn een handige manier om veelvoorkomende query-behoeften te extraheren naar welomschreven methoden van uw eigen. Op die manier kunnen ze wat gemakkelijker worden doorgegeven en kunnen ze mogelijk ook gemakkelijker worden begrepen als anderen met uw code moeten werken of als u in de toekomst bepaalde vragen opnieuw moet bekijken. U kunt ze voor afzonderlijke modellen definiëren, maar ze ook voor hun associaties gebruiken. 

De lucht is echt de limiet-doet mee, omvat, en waar zijn allemaal eerlijk spel! Omdat scopes ook terugkomen ActiveRecord :: Relations objecten, je kunt ze ketenen en zonder aarzelen andere scopes daar bovenop roepen. Zo scopes uitpakken en ze ketenen voor complexere query's is erg handig en maakt langere scanners beter leesbaar. Scopes worden gedefinieerd via de syntaxis "stabby lambda":

rails

klasse Missie < ActiveRecord::Base has_many: agents scope :successful, -> where (mission_complete: true) einde Mission.uccesvol
class Agent < ActiveRecord::Base belongs_to :mission scope :licenced_to_kill, -> where (licence_to_kill: true) scope: womanizer, -> where (womaniser: true) scope: gambler, -> where (gambler: true) end # Agent.gambler # Agent.womanizer # Agent.licenced_to_kill # Agent.womanizer.gambler Agent.licenced_to_kill.womanizer.gambler

SQL

SELECTEER "agents". * FROM "agents" WAAR "agents". "Licence_to_kill" =? EN "agenten". "Womanizer" =? EN "agenten". "Gokker" =? [["licence_to_kill", "t"], ["womanizer", "t"], ["gokker", "t"]]

Zoals je kunt zien aan de hand van het voorbeeld hierboven, is het vinden van James Bond veel leuker als je alleen scopes bij elkaar kunt zetten. Op die manier kunt u verschillende vragen combineren en matchen en tegelijkertijd DROOG blijven. Als u scopes nodig hebt via associaties, staan ​​ze ook tot uw beschikking:

Mission.last.agents.licenced_to_kill.womanizer.gambler
SELECTEER "missies". * FROM "missies" ORDER BY "missies". "Id" DESC LIMIT 1 SELECTEER "agents". * FROM "agents" WAAR "agents". "Mission_id" =? AND "agents". "Licence_to_kill" =? EN "agenten". "Womanizer" =? EN "agenten". "Gokker" =? [["mission_id", 33], ["licence_to_kill", "t"], ["womanizer", "t"], ["gokker", "t"]]

U kunt ook de. Herdefiniëren default_scope voor als je naar zoiets kijkt Mission.all.

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

SQL

 SELECTEER "missies". * FROM "missies" WAAR "missies". "Status" =? [["" status "," Bezig "]]

samenvoegingen

Deze sectie is niet zozeer gevorderd qua begrip, maar je zult ze vaker wel dan niet nodig hebben in scenario's die beschouwd kunnen worden als een beetje geavanceerder dan je gemiddelde vinder-achtige .allemaal, .eerste, .find_by_id of wat dan ook. Filteren op basis van basisberekeningen is bijvoorbeeld waarschijnlijk iets waar nieuwkomers niet meteen mee in contact komen. Waar kijken we precies naar hier?

  • som
  • tellen
  • minimum
  • maximum
  • gemiddelde

Makkelijk peasy, toch? Het leuke is dat we in plaats van een geretourneerde verzameling objecten door te laten lopen om deze berekeningen te doen, we Active Record al dit werk voor ons kunnen laten doen en deze resultaten met de query's kunnen retourneren - bij voorkeur in één query. Leuk, he?

  • tellen

rails

Mission.count # => 24

SQL

SELECTEER COUNT (*) VAN "missies"
  • gemiddelde

rails

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

SQL

SELECT AVG ("agents". "Number_of_gadgets") FROM "agenten"

Omdat we nu weten hoe we gebruik kunnen maken doet mee, we kunnen dit nog een stap verder doen en alleen vragen naar het gemiddelde aantal gadgets dat de agenten op een bepaalde missie hebben, bijvoorbeeld.

rails

Agent.joins (: missie) .where (missies: name: 'Moonraker'). Average (: number_of_gadgets) .to_f # => 3.4

SQL

SELECT AVG ("agents". "Number_of_gadgets") FROM "agents" INNER JOIN "missies" AAN "missies". "Id" = "agents". "Mission_id" WAAR "missies". "Naam" =? [["naam", "Moonraker"]]

Het groeperen van dit gemiddelde aantal gadgets op namen van missies wordt op dat moment triviaal. Meer informatie over onderstaande groepering:

rails

Agent.joins (: mission) .Group ( 'missions.name') gemiddelde. (: Number_of_gadgets)

SQL

SELECT AVG ("agents". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" GROUP BY missions.name
  • som

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 ("agents". "Number_of_gadgets") FROM "agents" SELECT SUM ("agents". "Number_of_gadgets") FROM "agents" WAAR "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]] SELECT SUM ("agents". "number_of_gadgets") FROM "agents" WHERE ("agents". "licence_to_kill"! =?) [["licence_to_kill", "t"]]
  • maximum

rails

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

SQL

SELECT MAX ("agents". "Number_of_gadgets") FROM "agents" SELECT MAX ("agents". "Number_of_gadgets") FROM "agents" WAAR "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]
  • minimum

rails

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

SQL

SELECT MIN ("agents". "Iq") FROM "agents" SELECT MIN ("agents". "Iq") FROM "agents" WAAR "agents". "Licence_to_kill" =? [["licence_to_kill", "t"]]

Aandacht!

Al deze aggregatiemethoden laten je niet aan andere dingen vastklampen: ze zijn terminal. De volgorde is belangrijk om berekeningen te maken. We krijgen geen ActiveRecord :: Relation object terug van deze bewerkingen, waardoor de muziek op dat moment stopt - we krijgen in plaats daarvan een hash of cijfers. De onderstaande voorbeelden werken niet:

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

gegroepeerd

Als u wilt dat de berekeningen worden opgesplitst en gesorteerd in logische groepen, moet u een GROEP clausule en doe dit niet in Ruby. Wat ik daarmee bedoel is dat je moet vermijden om een ​​groep te doorlopen die potentieel tonnen vragen produceert.

rails

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

SQL

SELECT AVG ("agents". "Number_of_gadgets") AS average_number_of_gadgets, missions.name AS missions_name FROM "agents" INNER JOIN "missions" ON "missions". "Id" = "agents". "Mission_id" GROUP BY missions.name

In dit voorbeeld worden alle agents gevonden die zijn gegroepeerd naar een bepaalde missie en wordt een hash geretourneerd met het berekende gemiddelde aantal gadgets als waarden - in één query! JEP! Hetzelfde geldt natuurlijk ook voor de andere berekeningen. In dit geval is het logischer om SQL het werk te laten doen. Het aantal vragen dat we voor deze samenvoegingen inzenden, is gewoon te belangrijk.

Dynamische zoekers

Voor elk kenmerk op uw modellen bijvoorbeeld naam, e-mailadresfavorite_gadget enz. Met Active Record kunt u zeer leesbare zoekmethoden gebruiken die dynamisch voor u worden gemaakt. Klinkt cryptisch, ik weet het, maar het betekent niets anders dan find_by_id of find_by_favorite_gadget. De find_by onderdeel is standaard en Active Record stopt gewoon de naam van het attribuut voor u. Je kunt zelfs een toevoegen ! als je wilt dat die vinder een fout maakt als er niets gevonden kan worden. Het zieke deel is, je kunt deze dynamische zoeker-methoden zelfs samen ketenen. Net als dit:

rails

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

SQL

SELECTEER "agents". * FROM "agents" WAAR "agents". "Name" =? LIMIT 1 [["naam", "James Bond"]] SELECT "agents". * FROM "agents" WAAR "agents". "Name" =? AND "agents". "Licence_to_kill" =? LIMIT 1 [["naam", "James Bond"], ["licence_to_kill", "t"]]

Natuurlijk kun je hiermee gek worden, maar ik denk dat het zijn charme en bruikbaarheid verliest als je verder gaat dan twee attributen:

rails

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

SQL

SELECTEER "agents". * FROM "agents" WAAR "agents". "Name" =? AND "agents". "Licence_to_kill" =? EN "agenten". "Womanizer" =? EN "agenten". "Gokker" =? AND "agents". "Number_of_gadgets" =? LIMIT 1 [["naam", "James Bond"], ["licence_to_kill", "t"], ["womanizer", "t"], ["gokker", "t"], ["number_of_gadgets", 3 ]]

In dit voorbeeld is het toch leuk om te zien hoe het werkt onder de motorkap. Elke nieuwe _en_ voegt een SQL toe EN operator om de attributen logisch samen te brengen. Over het algemeen is het belangrijkste voordeel van dynamische vinders de leesbaarheid - te veel dynamische attributen opbergen verliest dat voordeel echter snel. Ik gebruik dit zelden, misschien vooral als ik in de console speel, maar het is zeker goed om te weten dat Rails deze nette kleine bedrog biedt.

Specifieke velden

Active Record geeft u de mogelijkheid om objecten te retourneren die wat meer gericht zijn op de attributen die ze dragen. Meestal, als niet anders aangegeven, vraagt ​​de query alle velden in een rij via * (SELECTEER "agenten". *) en vervolgens genereert Active Record Ruby-objecten met de volledige set attributen. U kunt echter kiezen alleen specifieke velden die door de query moeten worden geretourneerd en beperk het aantal kenmerken dat uw Ruby-objecten nodig hebben om 'rond te dragen'.

rails

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

SQL

SELECTEER "agenten". "Naam" VAN "agenten"

rails

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

SQL

SELECTEER "agenten". "Aantal", "agenten". "Favoriet_gadget" VAN "agenten"

Zoals u kunt zien, hebben de geretourneerde objecten alleen de geselecteerde kenmerken, plus hun ID's natuurlijk, dat is een gegeven met elk object. Het maakt geen verschil of u strings gebruikt, zoals hierboven, of symbolen - de query zal hetzelfde zijn.

rails

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

Een woord van waarschuwing: als u toegang probeert te krijgen tot kenmerken van het object die u niet hebt geselecteerd in uw query's, ontvangt u een MissingAttributeError. Sinds de ID kaart zal hoe dan ook automatisch voor u worden aangeboden, u kunt de ID wel opvragen zonder deze te selecteren.

Aangepaste SQL

Last but not least, u kunt uw eigen aangepaste SQL via schrijven find_by_sql. Als je genoeg zelfvertrouwen hebt in je eigen SQL-Fu en wat aangepaste oproepen naar de database nodig hebt, kan deze methode soms erg handig zijn. Maar dit is een ander verhaal. Vergeet echter niet eerst te controleren op Active Record-wrappermethoden en vermijd het wiel opnieuw uit te vinden waar Rails u meer dan halverwege probeert te ontmoeten.

rails

Agent.find_by_sql ("SELECT * FROM agents") Agent.find_by_sql ("SELECT-naam, licentie_naar_kill VAN agents") 

Niet verrassend resulteert dit in:

SQL

SELECT * FROM agents SELECT name, licence_to_kill FROM agents

Omdat scopes en uw eigen klassemethoden uitwisselbaar kunnen worden gebruikt voor uw aangepaste zoekbehoeften, kunnen we dit een stap verder doen voor complexere SQL-query's. 

rails

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

We kunnen klassemethoden schrijven die de SQL in een document Here inkapselen. Hiermee kunnen we multi-line strings op een zeer leesbare manier schrijven en die SQL-string opslaan in een variabele die we opnieuw kunnen gebruiken en doorgeven find_by_sql. Op die manier gieten we niet veel query-code in de methodeaanroep. Als u meer dan één plaats heeft om deze query te gebruiken, is deze ook DROOG.

Omdat dit verondersteld wordt nieuwkomervriendelijk te zijn en geen SQL-tutorial per se, heb ik het voorbeeld om een ​​bepaalde reden erg minimalistisch gehouden. De techniek voor veel complexere vragen is echter hetzelfde. Het is gemakkelijk om je voor te stellen dat je een aangepaste SQL-query hebt die meer dan tien regels code omvat. 

Ga zo gek als je nodig hebt - redelijk! Het kan een reddingsboei zijn. Een woord over de syntaxis hier. De SQL part is hier slechts een identifier om het begin en einde van de string te markeren. Ik wed dat je deze methode niet zo nodig zult hebben - laten we hopen! Het heeft absoluut zijn plaats, en Rails land zou niet hetzelfde zijn zonder het - in de zeldzame gevallen dat je absoluut je eigen SQL absoluut wilt afstemmen met het.

Laatste gedachten

Ik hoop dat je wat meer op je gemak bent met het schrijven van vragen en het lezen van de gevreesde oude onbewerkte SQL. De meeste onderwerpen die we in dit artikel behandelen, zijn essentieel voor het schrijven van vragen die te maken hebben met complexere bedrijfslogica. Neem de tijd om deze te begrijpen en speel wat rond met vragen in de console. 

Ik ben er vrij zeker van dat wanneer je het studieland achterlaat, je Rails-crediteur vroeg of laat aanzienlijk zal stijgen als je aan je eerste echte projecten werkt en je eigen aangepaste queries moet maken. Als je nog steeds een beetje verlegen bent met het onderwerp, zou ik zeggen dat je er gewoon plezier in hebt - het is echt geen rocket science!