Förbättra prestanda av din Rails App med ivriga Laddar

Användare gillar flammande snabba applikationer, och sedan blir de kär i dem och gör dem till en del av sitt liv. Långsamma applikationer, å andra sidan, irriterar bara användare och förlorar intäkterna. I den här handledningen ska vi se till att vi inte förlorar mer pengar eller användare och förstår de olika sätten att förbättra prestanda.

Aktiva poster och ORM är mycket kraftfulla verktyg i Ruby on Rails, men bara om vi vet hur man släpper ut och använder den kraften. I början hittar du många sätt att utföra en liknande uppgift i RoR,men bara när du gräver lite djupare lär du dig faktiskt kostnaden för att använda en över en annan. 

Det är samma historia när det gäller ORM och Association in Rails. De gör verkligen livet enklare, men i vissa situationer kan de också fungera som överkill.

Låt oss ta ett exempel

Men innan det, låt oss snabbt generera en dummy applikation att leka med.

Steg 1 

Starta din terminal och skriv dessa kommandon för att skapa en ny applikation:

skenar ny blogg cd-blogg

Steg 2

Generera din ansökan:

skenor g ställning Författare namn: strängskenor g ställning Post titel: sträng kropp: text författare: referenser

Steg 3

Implementera den på din lokala server:

rake db: migrera rails s

Och det var det! Nu borde du ha en löpande applikation.

Så här ska både våra modeller (författare och post) se ut. Vi har inlägg som tillhör Författare, och vi har författare som kan ha många inlägg. Det här är den grundläggande föreningen / relationen mellan dessa två modeller som vi ska spela med.

# Post Modell klass Post < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end

Ta en titt på din "Posts Controller" -så här ska det se ut. Vårt fokus ligger endast på indexmetoden.

# Controller class PostsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end

Och sist men inte minst, vårt inlägg Index View. Din kan tyckas ha några extra linjer, men det är de jag vill att du ska fokusera på, speciellt linjen med post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Låt oss bara skapa några dummy data innan vi börjar. Gå till din rails konsol och lägg till följande rader. Eller du kan bara gå till http: // localhost: 3000 / inlägg / new och  http: // localhost: 3000 / författare / new att lägga till vissa data manuellt. 

authors = Author.create ([name: 'John', name: 'Doe', name: 'Manish']) Post.create (titel: 'I love Tuts +', body: authors.first) Post.create (titel: 'Tuts + is Awesome', body: ", författare: authors.second) Post.create (titel: 'Long Live Tuts +', body:", author: authorhorslast) 

Nu när du är upptagen, låt oss starta servern med skenor s och slå localhost: 3000 / inlägg.

Du får se några resultat på din skärm så här.

Så verkar allt bra: inga fel, och det hämtar alla poster tillsammans med de associerade författarnas namn. Men om du tittar på din utvecklingslogg ser du massor av frågor som exekveras som nedan.

Postload (0.6ms) VÄLJ "inlägg". * FRÅN "inlägg" ORDER BY "posts". "Created_at" DESC Author Load (0.5ms) VÄLJ "författare". * FRÅN "författare" VAR "författare". =? LIMIT 1 [["id", 3]] Författare Load (0.1ms) VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? LIMIT 1 [["id", 2]] Författarklass (0.1ms) VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? GRÄNS 1 [["id", 1]]

Tja, okej, jag håller med om det här är bara fyra frågor, men tänk dig att du har 3000 inlägg i din databas istället för bara tre. I så fall kommer vår databas att översvämmas med 3.000 + 1 frågor, varför det här problemet heter N + 1 problem.

Varför får vi det här problemet?

Så som standard i Ruby on Rails, har ORM lata laddningen aktiverad, vilket innebär att det försenar laddningen av data till den punkt där vi faktiskt behöver det.

I vårt fall är det först regulatorn där det ombeds att hämta alla inlägg.

def index @posts = Post.order (created_at:: desc) slutet

För det andra är vyn, där vi slingrar igenom de inlägg som hämtats av regulatorn och skickar en fråga för att få författarens namn för varje inlägg separat. Därav N + 1 problem. 

<% @posts.each do |post| %> ... <%= post.author.name %>  <% end %>

Hur löser vi problemet?

För att rädda oss från sådana situationer erbjuder Rails oss en funktion som heter ivrig lastning.

Med ivriga laddningar kan du förinställa de tillhörande data (författare)för alla inlägg från databasen, förbättrar övergripande prestanda genom att minska antalet frågor och ger dig de data som du vill visa i dina åsikter, men den enda fångsten här är vilken som ska användas. Fick dig!

Ja, för att vi har tre av dem, och alla tjänar samma syfte, men beroende på fallet kan någon av dem visa sig att minska eller överdriva prestanda igen.

förladdning () eager_load () innehåller ()

Nu kan du fråga vilken som ska användas i det här fallet? Tja, låt oss börja med den första.

def index @posts = Post.order (created_at:: desc) .preload (: author) slutet

Spara den. Hämta URL-adressen igen localhost: 3000 / inlägg.

Så inga förändringar i resultaten: allt laddas exakt på samma sätt, men under huven i utvecklingsloggen har dessa tonfrågor ändrats till följande två.

VÄLJ "inlägg". * FRÅN "författare" VAR "författare". "ID" IN (3, 2, 1)

Preload använder två separata frågor för att ladda huvuddata och tillhörande data. Det här är faktiskt mycket bättre än att ha en separat fråga för varje författarnamn (N + 1 Problem), men det räcker inte för oss. På grund av dess separata sökfrågor kommer det att slänga ett undantag i scenarier som:

  1. Beställ inlägg enligt författarens namn.
  2. Hitta inlägg från författaren "John" bara.

Låt oss försöka alla scenarier med eager_load () en efter en

1. Beställ inlägg enligt författarens namn

# Beställ inlägg enligt författarens namn. def index @posts = Post.order ("authors.name"). eager_load (: author) slutet

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". "Id" AS t0_r0, "inlägg". "Titel" AS t0_r1, "inlägg". "Body" AS t0_r2, "inlägg". "Author_id" AS t0_r3, "inlägg". "Created_at" AS t0_r4 , "författare". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "författare". "namn" AS t1_r1, "författare". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 FRÅN "inlägg" VÄNSTER UTGÅENDE FÖLJ "författare" PÅ "författare". "Id" = "inlägg". "Author_id" ORDER BY authors.name 

2. Hitta inlägg från författaren "John" Only

# Hitta inlägg från författaren "John" bara. def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") slutet

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". "Id" AS t0_r0, "inlägg". "Titel" AS t0_r1, "inlägg". "Body" AS t0_r2, "inlägg". "Author_id" AS t0_r3, "inlägg". "Created_at" AS t0_r4 , "författare". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "författare". "namn" AS t1_r1, "författare". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 FRÅN "inlägg" VÄNSTER UTFÖR JOIN "Författare" PÅ "Författare". "ID" = "Inlägg". "Author_id" WHERE (authors.name = 'Manish') BESTÄLL AV "inlägg". "Created_at" DESC 

3. N + 1 Scenario

def index @posts = Post.order (created_at:: desc) .eager_load (: author) slutet 

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". "Id" AS t0_r0, "inlägg". "Titel" AS t0_r1, "inlägg". "Body" AS t0_r2, "inlägg". "Author_id" AS t0_r3, "inlägg". "Created_at" AS t0_r4 , "författare". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "författare". "namn" AS t1_r1, "författare". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 FRÅN "inlägg" VÄNSTER UTGÅENDE FÖLJ "författare" PÅ "författare". "Id" = "inlägg". "Author_id" ORDER BY "inlägg". "Created_at" DESC 

Så om du tittar på de resulterande frågorna i alla tre scenarierna finns det två saker gemensamt. 

Först, eager_load () använder alltid VÄNSTER YTTRE JOIN vad som än är fallet. För det andra får det alla tillhörande data i en enda fråga, vilket säkert överträffar förspänning () metod i situationer där vi vill använda tillhörande data för extra uppgifter som beställning och filtrering. Men en enda fråga och VÄNSTER YTTRE JOIN kan också vara mycket dyrt i enkla scenarier som ovan, där allt du behöver är att filtrera författarna som behövs. Det är som att använda en bazooka för att döda en liten flyga.

Jag förstår att det bara är två enkla exempel, och i verkliga scenarier där ute kan det vara mycket svårt att bestämma den som är bäst för din situation. Så det är anledningen till att Rails har gett oss innefattar () metod.

Med innefattar (), Active Record tar hand om det tuffa beslutet. Det är smarterare än både förspänning () och eager_load () metoder och bestämmer vilken som ska användas på egen hand.

Låt oss försöka alla scenarier med inkluderar ()

1. Beställ inlägg enligt författarens namn

# Beställ inlägg enligt författarens namn. def index @posts = Post.order ("authors.name"). innehåller (: författare) slutet

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". "Id" AS t0_r0, "inlägg". "Titel" AS t0_r1, "inlägg". "Body" AS t0_r2, "inlägg". "Author_id" AS t0_r3, "inlägg". "Created_at" AS t0_r4 , "författare". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "författare". "namn" AS t1_r1, "författare". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 FRÅN "inlägg" VÄNSTER UTGÅENDE FÖLJ "författare" PÅ "författare". "Id" = "inlägg". "Author_id" ORDER BY authors.name

2. Hitta inlägg från författaren "John" Only

# Hitta inlägg från författaren "John" bara. def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # För rails 4 Glöm inte att lägga till .references (: author ) i slutet @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish").

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". "Id" AS t0_r0, "inlägg". "Titel" AS t0_r1, "inlägg". "Body" AS t0_r2, "inlägg". "Author_id" AS t0_r3, "inlägg". "Created_at" AS t0_r4 , "författare". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "författare". "namn" AS t1_r1, "författare". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 FRÅN "inlägg" VÄNSTER UTFÖR JOIN "Författare" PÅ "Författare". "ID" = "Inlägg". "Author_id" WHERE (authors.name = 'Manish') BESTÄLL AV "inlägg". "Created_at" DESC 

3. N + 1 Scenario

def index @posts = Post.order (created_at:: desc) .includes (: author) slutet 

Resultatande sökning i utvecklingsloggarna:

VÄLJ "inlägg". * FRÅN "författare" VAR "författare". "ID" IN (3, 2, 1)

Nu om vi jämför resultaten med eager_load () metod, de två första fallen har liknande resultat, men i det sistnämnda fallet beslutade det smart att byta till förspänning () metod för bättre prestanda.

Awesome, Right?

Nej, för i den här prestationsprestandan kan det ibland också vara svårt att ladda. Jag hoppas att några av er redan har märkt att när ivriga laddningsmetoder används ANSLUTER SIG, de använder bara VÄNSTER YTTRE JOIN. Också i alla fall laddar de för mycket onödiga data i minnet - de väljer varje enskild kolumn från bordet, medan vi bara behöver författarens namn.

Välkommen till Joins

Även om Active Record kan du ange villkoren för de ivriga laddade föreningarna precis som ansluter sig (), Det rekommenderade sättet är att använda anslutningar istället. ~ Rails Dokumentation.

Som rekommenderas i skenan dokumentationen, den ansluter sig () Metoden är ett steg framåt i dessa situationer. Den ansluts till det associerade tabellen, men laddar bara nödvändiga modelldata till minne som inlägg i vårat fall. Därför laddar vi inte överflödiga data i minnet utan att vi kan göra det också.

Låt oss dyka i några exempel

1. Beställ inlägg enligt författarens namn

# Beställ inlägg enligt författarens namn. def index @posts = Post.order ("authorhorsname"). slutar (: författare) slut

Resultatande sökning i utvecklingsloggarna:

SELECT "posts". * FRÅN "posts" INNER JOIN "author" ON "author". "Id" = "posts". "Author_id" ORDER BY authors.name VÄLJ "författare". * FRÅN "författare" VAR "författare" . "id" =? LIMIT 1 [["id", 2]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? LIMIT 1 [["id", 1]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? GRÄNS 1 [["id", 3]]

2. Hitta inlägg från författaren "John" Only

# Hitta inlägg från författaren "John" bara. def index @posts = Post.order (published_at:: desc) .joins (: author) .where ("authors.name =?", "John") slut

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". * FRÅN "inlägg" INNER GÅNG "författare" PÅ "författare". "Id" = "inlägg". "Author_id" WHERE (authors.name = 'Manish') BESTÄLL AV "inlägg". "Created_at" DESC VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? GRÄNS 1 [["id", 3]] 

3. N + 1 Scenario

def index @posts = Post.order (published_at:: desc) .joins (: author) slutet

Resultatande sökning i utvecklingsloggarna:

SELECT "inlägg". * FRÅN "inlägg" INNER JOIN "author" på "författare". "Id" = "inlägg". "Author_id" ORDER BY "inlägg". "Created_at" DESC SELECT "författare". * FRÅN "författare "VAR" författare "." Id "=? LIMIT 1 [["id", 3]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? LIMIT 1 [["id", 2]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? GRÄNS 1 [["id", 1]]

Det första du kanske märker från resultaten ovan är att N + 1 Problemet är tillbaka, men låt oss fokusera på den goda delen först. 

Låt oss titta på den första frågan från alla resultat. Alla ser mer eller mindre ut så här. 

VÄLJ "inlägg". * FRÅN "inlägg" INNER JOIN "författare" PÅ "författare". "Id" = "inlägg". "Author_id" ORDER BY authors.name

Det hämtar alla kolumner från inlägg. Det ansluter sig fint både i tabellerna och sorterar eller filtrerar posterna beroende på tillståndet, men utan att hämta några data från det tillhörande tabellen. Det är vad vi ville ha i första hand.

Men efter de första frågorna ser vi 1 eller 3 eller N antal frågor beroende på data i din databas, så här:

VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? LIMIT 1 [["id", 2]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? LIMIT 1 [["id", 1]] VÄLJ "författare". * FRÅN "författare" VAR "författare". "Id" =? GRÄNS 1 [["id", 3]]

Nu kanske du frågar: varför är det här N + 1 problem tillbaka? Det är på grund av denna linje enligt vår åsikt post.author.name.

 <% @posts.each do |post| %>  <%= post.title %> <%= post.body %> <%= post.author.name %>  <% end %>  

Den här raden utlöser alla dessa frågor. Så i exemplet där vi bara beställde våra inlägg behöver vi inte visa författarnamnet i våra åsikter. I så fall kan vi åtgärda problemet genom att ta bort raden post.author.name från utsikten.

Men då kanske du frågar, "Hej MK, hur är det med exemplen där vi vill visa författarens namn i vyn?" 

Tja, i så fall, ansluter sig () Metoden kommer inte att fixa det själv. Vi måste berätta ansluter sig () för att välja författarens namn eller någon annan kolumn från tabellen för den delen. Och vi kan göra det genom att lägga till en Välj() uttalande i slutet, så här:

def index @posts = Post.order (published_at:: desc) .joins (: author) .select ("inlägg. *, författare.namn som författarnamn") slut

Jag skapade ett alias "author_name" för authors.name. Vi får se varför i bara en sekund.

Resultatande sökning i utvecklingsloggarna:

SELECT inlägg. *, Författare.namn som författarnamn FRÅN "inlägg" INNER JOIN "författare" PÅ "författare". "Id" = "inlägg". "Author_id" ORDER BY "inlägg". "Created_at" DESC 

Här går vi: äntligen en ren SQL-fråga med nej N + 1 problem, utan onödiga data, med bara de saker vi behöver. Det enda som kvar är att använda det aliaset som du ser och ändra post.author.name till post.author_name. Detta beror på att författarnamnet nu är ett attribut för vår Post-modell, och efter den här ändringen är det här hur sidan ser ut:

Allt exakt detsamma, men under huven ändrades mycket saker. Om jag lägger allt i ett nötskal, för att lösa N + 1 du borde gå för ivrig lastning, men ibland, beroende på situationen, bör du ta saker i din kontroll och användning förenar för bättre alternativ. Du kan också leverera råa SQL-frågor till ansluter sig () metod för mer anpassning.

Sammanfogning och ivrig lastning tillåter också laddning av flera föreningar, men i början kan saker bli mycket komplicerade och svåra att bestämma det bästa alternativet. I sådana situationer rekommenderar jag att du läser dessa två väldigt trevliga Envato Tuts + handledning för att få en bättre förståelse för att gå och kunna bestämma det billigaste sättet när det gäller prestanda:

  • En djupare titt på avancerade valda sökfrågor 
  • Arbetar med MySQL och INNER JOIN

Sist men inte minst kan det vara komplicerat att ta reda på områden i din pre-build applikation där du borde förbättra prestanda generellt eller hitta N + 1 problem. I dessa fall rekommenderar jag en fin pärla som heter Kula. Det kan meddela dig när du ska lägga till ivriga laddningar för N + 1 frågor och när du använder ivriga lastning i onödan.