Forbedre ytelsen til dine skinner App med ivrig lasting

Brukere liker flammende raske applikasjoner, og så blir de forelsket i dem og gjør dem til en del av livet deres. Sakte applikasjoner, derimot, bare irriterer brukere og mister inntekter. I denne opplæringen skal vi sørge for at vi ikke mister mer penger eller brukere, og forstår de ulike måtene å forbedre ytelsen.

Active Records og ORM er svært kraftige verktøy i Ruby on Rails, men bare hvis vi vet hvordan du kan slippe løs og bruke den kraften. I begynnelsen finner du mange måter å utføre en lignende oppgave i RoR,men bare når du graver litt dypere, får du faktisk vite kostnadene ved å bruke en over en annen. 

Det er den samme historien når det gjelder ORM og Foreninger i Rails. De gjør at livet vårt blir enklere, men i noen situasjoner kan det også fungere som overkill.

La oss ta et eksempel

Men før det, la oss raskt generere en dummy applikasjon å leke med.

Trinn 1 

Start opp terminalen din og skriv inn disse kommandoene for å opprette et nytt program:

skinner ny blogg cd blogg

Steg 2

Generer søknaden din:

skinner g stillas Forfatternavn: strengskinner g stillas Posttittel: strenglegeme: tekstforfatter: referanser

Trinn 3

Distribuer den på din lokale server:

rake db: migrere skinner s

Og det var det! Nå bør du ha en løpende dummy applikasjon.

Slik ser begge modellene våre (forfatter og innlegg) ut. Vi har Innlegg som tilhører Forfatter, og vi har Forfattere som kan ha mange innlegg. Dette er den helt grunnleggende foreningen / relasjonen mellom disse to modellene som vi skal spille med.

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

Ta en titt på "Innleggsstyreren" - dette er hvordan det skal se ut. Vårt hovedfokus vil bare være på indeksmetoden.

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

Og sist men ikke minst, vårt innleggs indeksvisning. Din kan virke å ha noen ekstra linjer, men disse er de jeg vil at du skal fokusere på, spesielt linjen med post.author.name.

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

La oss bare lage noen dummy data før vi kommer i gang. Gå til skinner konsollen din og legg til følgende linjer. Eller du kan bare gå til http: // localhost: 3000 / innlegg / ny og  http: // localhost: 3000 / forfattere / ny å legge til noen data manuelt. 

Forfatter = Forfatter.skap ([navn: 'John', navn: 'Doe', navn: 'Manish']) Post.create (tittel: 'I love Tuts +', body: forfatter.first) Post.create (tittel: 'Tuts + is Awesome', body: ", forfatter: authors.second) Post.create (tittel: 'Long Live Tuts +', body:", author: authors.last) 

Nå som du er satt opp, la oss starte serveren med skinner s og treffer localhost: 3000 / innlegg.

Du får se noen resultater på skjermen din som dette.

Så alt virker bra: ingen feil, og det henter alle postene sammen med de tilhørende forfatternavnene. Men hvis du tar en titt på utviklingsloggen, ser du tonnevis av spørringer som blir utført som nedenfor.

Postload (0.6ms) SELECT "posts". * FRA "innlegg" ORDER BY "posts". "Created_at" DESC Author Load (0.5ms) VELG "forfattere". * FRA "forfattere" hvor "forfattere". =? LIMIT 1 [["id", 3]] Forfatterlast (0.1ms) VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 2]] Forfatterlast (0.1ms) VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 1]]

Vel, ok, jeg er enig i at dette er bare fire spørringer, men tenk at du har 3000 innlegg i databasen din i stedet for bare tre. I så fall vil vår database bli oversvømmet med 3000 + 1 spørringer, og derfor kalles dette problemet N + 1 problem.

Hvorfor får vi dette problemet?

Så som standard i Ruby on Rails, har ORM lazy loading aktivert, noe som betyr at det forsinker lasting av data til det punktet der vi faktisk trenger det.

I vårt tilfelle er det først kontrolleren hvor det blir bedt om å hente alle innleggene.

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

For det andre er visningen, hvor vi løp gjennom innleggene hentet av kontrolleren og send en forespørsel for å få forfatternavnet for hvert innlegg separat. Derav N + 1 problem. 

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

Hvordan løser vi problemet?

For å redde oss fra slike situasjoner, tilbyr Rails oss en funksjon som heter ivrig lasting.

Med påkrevd lasting kan du forhåndsbelaste de tilknyttede dataene (forfattere)for alle innlegg fra databasen, forbedrer den generelle ytelsen ved å redusere antall spørringer, og gir deg de dataene du vil vise i visningene dine, men den eneste fangsten her er den som skal brukes. Tok deg!

Ja fordi vi har tre av dem, og alle tjener samme formål, men avhengig av saken kan noen av dem redusere eller overkillere ytelsen igjen.

preload () eager_load () inkluderer ()

Nå kan du spørre hvilken du skal bruke i dette tilfellet? Vel, la oss starte med den første.

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

Lagre det. Treff nettadressen igjen localhost: 3000 / innlegg.

Så ingen endringer i resultatene: alt laster akkurat på samme måte, men under hetten i utviklingsloggen er disse tonnene av spørsmål blitt endret til følgende to.

SELECT "innlegg". * FRA "innlegg" ORDER BY "posts". "Created_at" DESC SELECT "forfattere". * FRA "forfattere" hvor "forfattere". "ID" IN (3, 2, 1)

Preload bruker to separate spørringer for å laste hoveddata og tilhørende data. Dette er faktisk mye bedre enn å ha en separat spørring for hvert forfatternavn (N + 1 Problem), men dette er ikke nok for oss. På grunn av sin egen spørsmålsstrategi vil det kaste et unntak i scenarier som:

  1. Bestill innlegg etter forfatterens navn.
  2. Finn innlegg fra forfatteren "John" bare.

La oss prøve alle scenarier med eager_load () En etter en

1. Bestill innlegg etter forfatterens navn

# Bestill innlegg etter forfatterens navn. def index @posts = Post.order ("authorhorsname"). eager_load (: author) end

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". "ID" AS t0_r0, "innlegg". "Tittel" AS t0_r1, "innlegg". "Body" AS t0_r2, "innlegg". "Author_id" AS t0_r3, "innlegg". "Created_at" AS t0_r4 , "forfattere". "created_at" AS t1_r2, "forfattere". "updated_at" AS t1_r3, "author". "updated_at" AS t0_r5, "authors". FRA "innlegg" VENSTRE YTRE KJØP "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY authors.name 

2. Finn innlegg fra forfatteren "John" Only

# Finn innlegg fra forfatteren "John" bare. def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") slutten

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". "ID" AS t0_r0, "innlegg". "Tittel" AS t0_r1, "innlegg". "Body" AS t0_r2, "innlegg". "Author_id" AS t0_r3, "innlegg". "Created_at" AS t0_r4 , "forfattere". "created_at" AS t1_r2, "forfattere". "updated_at" AS t1_r3, "author". "updated_at" AS t0_r5, "authors". FRA "innlegg" VENSTRE YTRE KJØP "Forfattere" PÅ "Forfattere". "ID" = "Innlegg". "Author_id" WHERE (authors.name = 'Manish') BESTILL BY "innlegg". "Created_at" DESC 

3. N + 1 Scenario

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

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". "ID" AS t0_r0, "innlegg". "Tittel" AS t0_r1, "innlegg". "Body" AS t0_r2, "innlegg". "Author_id" AS t0_r3, "innlegg". "Created_at" AS t0_r4 , "forfattere". "created_at" AS t1_r2, "forfattere". "updated_at" AS t1_r3, "author". "updated_at" AS t0_r5, "authors". FRA "innlegg" VENSTRE YTRE KJØP "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY "innlegg". "Created_at" DESC 

Så hvis du ser på de resulterende spørringene i alle tre scenariene, er det to ting til felles. 

Først, eager_load () bruker alltid VENSTRE YTTRE JOIN uansett hva som måtte være tilfelle. For det andre får det alle de tilknyttede dataene i en enkelt spørring, som sikkert overgår forbelastning () metode i situasjoner der vi vil bruke de tilknyttede dataene for ekstra oppgaver som bestilling og filtrering. Men en enkelt spørring og VENSTRE YTTRE JOIN kan også være veldig dyrt i enkle scenarier som ovenfor, der alt du trenger er å filtrere forfatterne som trengs. Det er som å bruke en bazooka for å drepe en liten fly.

Jeg forstår at disse bare er to enkle eksempler, og i ekte scenarier der ute kan det være svært vanskelig å bestemme seg for den som er best for din situasjon. Så det er grunnen til at Rails har gitt oss innbefatter () metode.

Med innbefatter (), Active Record tar seg av den tøffe beslutningen. Det er langt smartere enn begge forbelastning () og eager_load () metoder og bestemmer hvilken som skal brukes på egenhånd.

La oss prøve alle scenarier med inkluderer ()

1. Bestill innlegg etter forfatterens navn

# Bestill innlegg etter forfatterens navn. def index @posts = Post.order ("authors.name"). inkluderer (: forfatter) slutt

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". "ID" AS t0_r0, "innlegg". "Tittel" AS t0_r1, "innlegg". "Body" AS t0_r2, "innlegg". "Author_id" AS t0_r3, "innlegg". "Created_at" AS t0_r4 , "forfattere". "created_at" AS t1_r2, "forfattere". "updated_at" AS t1_r3, "author". "updated_at" AS t0_r5, "authors". FRA "innlegg" VENSTRE YTRE KJØP "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY authors.name

2. Finn innlegg fra forfatteren "John" Only

# Finn innlegg fra forfatteren "John" bare. def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # For skinner 4 Ikke glem å legge til .references (: author ) i slutten @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish"). referanser (: forfatter) ende

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". "ID" AS t0_r0, "innlegg". "Tittel" AS t0_r1, "innlegg". "Body" AS t0_r2, "innlegg". "Author_id" AS t0_r3, "innlegg". "Created_at" AS t0_r4 , "forfattere". "created_at" AS t1_r2, "forfattere". "updated_at" AS t1_r3, "author". "updated_at" AS t0_r5, "authors". FRA "innlegg" VENSTRE YTRE KJØP "Forfattere" PÅ "Forfattere". "ID" = "Innlegg". "Author_id" WHERE (authors.name = 'Manish') BESTILL BY "innlegg". "Created_at" DESC 

3. N + 1 Scenario

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

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". * FRA "innlegg" ORDER BY "posts". "Created_at" DESC SELECT "forfattere". * FRA "forfattere" hvor "forfattere". "ID" IN (3, 2, 1)

Nå hvis vi sammenligner resultatene med eager_load () metode, de to første sakene har lignende resultater, men i det siste tilfellet bestemte det seg smart å skifte til forbelastning () metode for bedre ytelse.

Awesome, Right?

Nei, for i dette løpene med ytelse kan noen ganger ivrig lasting også bli kort. Jeg håper noen av dere allerede har lagt merke til at når ivrige lastningsmetoder bruker tiltrer, de bruker bare VENSTRE YTTRE JOIN. Også i alle tilfeller laster de for mye unødvendig data i minnet - de velger hver eneste kolonne fra bordet, mens vi bare trenger forfatterens navn.

Velkommen til samlingene

Selv om Active Record lar deg spesifisere forholdene på de ivrige belastede foreningene akkurat som tiltrer (), Den anbefalte måten er å bruke tilkoblinger i stedet. ~ Rails Dokumentasjon.

Som anbefalt i skinner dokumentasjonen, den tiltrer () Metoden er et skritt foran i disse situasjonene. Den knytter seg til det tilknyttede tabellen, men laster bare de nødvendige modelldataene inn i minnet som innlegg i vårt tilfelle. Derfor laster vi ikke overflødige data i minnet unødvendig - selv om vi vil at vi kan gjøre det også.

La oss dykke inn i noen eksempler

1. Bestill innlegg etter forfatterens navn

# Bestill innlegg etter forfatterens navn. def index @posts = Post.order ("authors.name"). slutter (: forfatter) slutten

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". * FRA "innlegg" INNER KJØP "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY authors.name VELGE "forfattere". * FRA "forfattere" hvor "forfattere" . "id" =? LIMIT 1 [["id", 2]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 1]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 3]]

2. Finn innlegg fra forfatteren "John" Only

# Finn innlegg fra forfatteren "John" bare. def index @posts = Post.order (published_at:: desc) .joins (: author) .where ("authors.name =?", "John") slutten

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". * FRA "Innlegg" INNER KJØP "Forfattere" PÅ "Forfattere". "ID" = "Innlegg". "Author_id" WHERE (authors.name = 'Manish') BESTILL BY "innlegg". "Created_at" DESC VELG "forfattere". * FRA "forfattere" der "forfattere". "Id" =? LIMIT 1 [["id", 3]] 

3. N + 1 Scenario

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

Resulterende spørring i utviklingsloggene:

SELECT "innlegg". * FRA "innlegg" INNER KJØP "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY "innlegg". "Created_at" DESC SELECT "forfattere". * FRA "forfattere "WHERE" forfattere "." Id "=? LIMIT 1 [["id", 3]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 2]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 1]]

Det første du kanskje legger merke til fra resultatene ovenfor er at N + 1 Problemet er tilbake, men la oss fokusere på den gode delen først. 

La oss se på det første spørsmålet fra alle resultatene. Alle av dem ser mer eller mindre ut som dette. 

SELECT "innlegg". * FRA "innlegg" INNER JOIN "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY authors.name

Den henter alle kolonnene fra innlegg. Det knytter seg godt til begge tabellene, og sorterer eller filtrerer postene avhengig av tilstanden, men uten å hente data fra tilhørende tabell. Som er det vi ønsket i utgangspunktet.

Men etter de første spørringene ser vi 1 eller 3 eller N Antall spørringer avhengig av dataene i databasen din, slik som dette:

VELG "forfattere". * FRA "forfattere" der "forfattere". "Id" =? LIMIT 1 [["id", 2]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 1]] VELG "forfattere". * FRA "forfattere" hvor "forfattere". "Id" =? LIMIT 1 [["id", 3]]

Nå kan du spørre: Hvorfor er dette N + 1 problemet tilbake? Det er på grunn av denne linjen i vår oppfatning post.author.name.

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

Denne linjen utløser alle disse spørsmålene. Så i eksemplet der vi bare måtte bestille innleggene våre, trenger vi ikke å vise forfatternavnet i våre synspunkter. I så fall kan vi løse dette problemet ved å fjerne linjen post.author.name fra utsikten.

Men så kan du spørre, "Hei MK, hva med eksemplene hvor vi vil vise forfatterens navn i visningen?" 

Vel, i så fall, tiltrer () Metoden kommer ikke til å løse det selv. Vi må fortelle tiltrer () for å velge forfatterens navn eller en annen kolonne fra tabellen for den saks skyld. Og vi kan gjøre det ved å legge til en å velge() uttalelse på slutten, slik:

def index @posts = Post.order (published_at:: desc) .joins (: author) .select ("innlegg. *, forfattere.navn som forfatternavn") slutte

Jeg opprettet et alias "author_name" for authors.name. Vi ser hvorfor i bare et sekund.

Resulterende spørring i utviklingsloggene:

SELECT innlegg. *, Forfattere.navn som forfatternavn FRA "innlegg" INNER JOIN "forfattere" PÅ "forfattere". "Id" = "innlegg". "Author_id" ORDER BY "innlegg". "Created_at" DESC 

Her går vi: endelig en ren SQL-spørring med nei N + 1 problem, uten unødvendige data, med bare de tingene vi trenger. Det eneste som er igjen er å bruke det aliaset i visningen din og endre post.author.name til post.author_name. Dette skyldes at forfatternavnet nå er et attributt til vår Post-modell, og etter denne endringen er dette hvordan siden ser ut:

Alt akkurat det samme, men under hetten ble mange ting forandret. Hvis jeg legger alt i et nøtteskall, for å løse N + 1 du bør gå for ivrig lasting, men til tider, avhengig av situasjonen, bør du ta ting i din kontroll og bruk tiltrer for bedre alternativer. Du kan også levere røde SQL-spørringer til tiltrer () metode for mer tilpasning.

Sammenføyning og ivrig lasting tillater også lasting av flere foreninger, men i begynnelsen kan ting bli svært komplisert og vanskelig å bestemme det beste alternativet. I slike situasjoner anbefaler jeg at du leser disse to veldig hyggelige Envato Tuts + opplæringene for å få bedre forståelse av sammenhenger og være i stand til å bestemme den minst kostbare tilnærmingen når det gjelder ytelse:

  • En dypere titt på avanserte velg spørringer 
  • Arbeider med MySQL og INNER JOIN

Sist men ikke minst kan det være komplisert å finne ut områder i ditt pre-build-program der du bør forbedre ytelsen generelt eller finne den N + 1 problemer. I disse tilfellene anbefaler jeg en fin perle som heter Kule. Det kan varsle deg når du skal legge til ivrig lasting for N + 1 spørringer, og når du bruker ivrig lasting unødvendig.