Gebruikers houden van razendsnelle toepassingen en worden vervolgens verliefd op ze en maken ze deel van hun leven. Langzame applicaties daarentegen irriteren alleen gebruikers en verliezen inkomsten. In deze zelfstudie gaan we ervoor zorgen dat we niet meer geld of gebruikers verliezen en begrijpen we de verschillende manieren om de prestaties te verbeteren.
Active Records en ORM zijn zeer krachtige tools in Ruby on Rails, maar alleen als we weten hoe we die kracht kunnen ontketenen en gebruiken. In het begin zul je veel manieren vinden om een vergelijkbare taak uit te voeren in RoR,maar alleen als je een beetje dieper graaft, leer je eigenlijk de kosten van het gebruik van de een boven de andere.
Het is hetzelfde verhaal in het geval van ORM en verenigingen in Rails. Ze maken ons leven zeker een stuk eenvoudiger, maar kunnen in sommige situaties ook als overkill fungeren.
Maar laten we eerder een dummy-applicatie maken om mee te spelen.
Start uw terminal en typ deze opdrachten om een nieuwe applicatie te maken:
rails nieuwe blog cd blog
Genereer uw applicatie:
rails g scaffold Naam auteur: string rails g steiger Titel van de post: string body: tekst auteur: referenties
Implementeer het op uw lokale server:
hark db: rails migreren s
En dat was het! Nu zou je een draaiende dummy-applicatie moeten hebben.
Dit is hoe beide modellen (Auteur en Post) eruit zouden moeten zien. We hebben berichten die eigendom zijn van de auteur en we hebben auteurs die veel berichten kunnen hebben. Dit is de basisrelatie tussen deze twee modellen waarmee we gaan spelen.
# Post Model class Post < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end
Kijk eens naar je "Posts Controller" - zo zou het eruit moeten zien. Onze belangrijkste focus ligt alleen op de indexmethode.
# Controller class PostsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end
En last but not least, onze Posts Index View. De jouwe lijkt misschien wat extra lijnen te hebben, maar dit zijn de lijnen waarop ik wil focussen, vooral de regel post.author.name
.
<% @posts.each do |post| %><% end %> <%= post.title %> <%= post.body %> <%= post.author.name %>
Laten we eerst wat dummygegevens maken voordat we aan de slag gaan. Ga naar je railsconsole en voeg de volgende regels toe. Of je kunt gewoon naar gaan http: // localhost: 3000 / berichten / nieuw
en http: // localhost: 3000 / auteurs / new
om sommige gegevens handmatig toe te voegen.
authors = Author.create ([name: 'John', name: 'Doe', name: 'Manish']) Post.create (title: 'I love Tuts +', body: ", auteur: authors.first) Post.create (titel: 'Tuts + is Awesome', body: ", auteur: authors.second) Post.create (titel: 'Long Live Tuts +', body:", auteur: authors.last)
Nu we allemaal zijn ingesteld, laten we de server starten met rails s
en druk op localhost: 3000 / berichten
.
Op deze manier ziet u enkele resultaten op uw scherm.
Dus alles lijkt in orde: geen fouten, en het haalt alle records samen met de bijbehorende auteursnamen. Maar als u uw ontwikkelingslogboek bekijkt, ziet u heel veel zoekopdrachten die worden uitgevoerd, zoals hieronder.
Post Load (0.6ms) SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC Author Load (0.5ms) SELECTEER "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIET 1 [["id", 3]] Auteur Belasting (0.1ms) SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIT 1 [["id", 2]] Author Load (0.1ms) SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIET 1 [["id", 1]]
Nou, oke, ik ben het ermee eens dat dit slechts vier vragen zijn, maar stel je voor dat je 3.000 berichten in je database hebt in plaats van slechts drie. In dat geval zal onze database overspoeld worden met 3000 + 1 zoekopdrachten, daarom wordt dit probleem het N + 1
probleem.
Dus in Ruby on Rails heeft de ORM lui laden ingeschakeld, wat betekent dat het laden van gegevens wordt vertraagd tot het moment dat we het echt nodig hebben.
In ons geval is het eerst de controller waar wordt gevraagd om alle berichten op te halen.
def index @posts = Post.order (created_at:: desc) einde
Ten tweede is de weergave, waar we door de berichten lopen die door de controller zijn opgehaald en een query verzenden om de auteursnaam voor elke post afzonderlijk te krijgen. Vandaar de N + 1
probleem.
<% @posts.each do |post| %>... <% end %><%= post.author.name %>
Om ons uit dergelijke situaties te redden, biedt Rails ons een functie genaamd gretig laden.
Met de functie Eager laden kunt u de bijbehorende gegevens vooraf laden (Auteurs)voor alle posts uit de database verbetert de algehele prestatie door het aantal query's te verminderen en biedt u de gegevens die u in uw weergaven wilt weergeven, maar de enige vangst hier is welke moet worden gebruikt. Gotcha!
Ja, omdat we er drie hebben, en allemaal hetzelfde doel dienen, maar afhankelijk van het geval, kan het zijn dat het de prestaties weer doet afnemen of overkoken.
preload () eager_load () includes ()
Nu zou je kunnen vragen welke je in dit geval moet gebruiken? Nou, laten we beginnen met de eerste.
def index @posts = Post.order (created_at:: desc) .preload (: author) end
Bewaar het. Raak de URL opnieuw aan localhost: 3000 / berichten
.
Dus geen wijzigingen in de resultaten: alles laadt precies op dezelfde manier, maar onder de motorkap in het ontwikkelingslogboek, zijn die tonnen vragen veranderd in de volgende twee.
SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" IN (3, 2, 1)
Preload gebruikt twee afzonderlijke query's om de hoofdgegevens en de bijbehorende gegevens te laden. Dit is eigenlijk veel beter dan een afzonderlijke zoekopdracht voor elke auteursnaam (het N + 1-probleem), maar dit is niet genoeg voor ons. Vanwege de afzonderlijke queryaanpak zal het een uitzondering in scenario's als:
# Bestel berichten op auteursnaam. def index @posts = Post.order ("authors.name"). eager_load (: auteur) einde
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "berichten". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "authors". "name" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 VAN "berichten" LINKER BUITEN KOPEN "auteurs" AAN "auteurs". "Id" = "berichten". "Auteur_id" BESTELLING BY authors.name
# Zoek alleen berichten van de auteur "John". def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") einde
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "berichten". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "authors". "name" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 VAN "berichten" LEFT OUTER JOIN "auteurs" OP "auteurs". "Id" = "berichten". "Auteur_id" WHERE (authors.name = 'Manish') BESTELLING OP "berichten". "Created_at" DESC
def index @posts = Post.order (created_at:: desc) .eager_load (: auteur) einde
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "berichten". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "authors". "name" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 VAN "berichten" LEFT OUTER JOIN "authors" ON "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC
Dus als u de resulterende query's van alle drie de scenario's bekijkt, zijn er twee dingen gemeen.
Eerste, eager_load ()
gebruikt altijd de LINKER BUITEN DOE MEE
hoe het ook zij. Ten tweede krijgt het alle bijbehorende gegevens in één query, wat zeker de outruns overtreft preload ()
methode in situaties waarin we de bijbehorende gegevens willen gebruiken voor extra taken zoals ordenen en filteren. Maar een enkele zoekopdracht en LINKER BUITEN DOE MEE
kan ook erg duur zijn in eenvoudige scenario's zoals hierboven, waar het enige wat je nodig hebt is om de benodigde auteurs te filteren. Het is net of je een bazooka gebruikt om een kleine vlieg te doden.
Ik begrijp dat dit slechts twee eenvoudige voorbeelden zijn, en in echte scenario's die er zijn, kan het heel moeilijk zijn om te beslissen welke het beste is voor uw situatie. Dat is de reden waarom Rails ons de omvat ()
methode.
Met omvat ()
, Active Record zorgt voor de moeilijke beslissing. Het is veel slimmer dan allebei preload ()
en eager_load ()
methoden en beslist welke te gebruiken op zichzelf.
# Bestel berichten op auteursnaam. def index @posts = Post.order ("authors.name"). includes (: auteur) einde
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "berichten". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "authors". "name" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 VAN "berichten" LINKER BUITEN KOPEN "auteurs" AAN "auteurs". "Id" = "berichten". "Auteur_id" BESTELLING BY authors.name
# Zoek alleen berichten van de auteur "John". def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # For rails 4 Vergeet niet om .references toe te voegen (: auteur ) uiteindelijk @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish"). references (: author) end
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". "Id" AS t0_r0, "posts". "Title" AS t0_r1, "posts". "Body" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "berichten". "updated_at" AS t0_r5, "authors". "id" AS t1_r0, "authors". "name" AS t1_r1, "authors". "created_at" AS t1_r2, "authors". "updated_at" AS t1_r3 VAN "berichten" LEFT OUTER JOIN "auteurs" OP "auteurs". "Id" = "berichten". "Auteur_id" WHERE (authors.name = 'Manish') BESTELLING OP "berichten". "Created_at" DESC
def index @posts = Post.order (created_at:: desc). includes (: author) end
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" IN (3, 2, 1)
Als we de resultaten vergelijken met de eager_load ()
methode, de eerste twee gevallen hebben vergelijkbare resultaten, maar in het laatste geval besloot het slim om over te schakelen naar de preload ()
methode voor betere prestaties.
Nee, want in deze prestatiewedstrijd kan soms ook een gretige belasting tekortschieten. Ik hoop dat sommigen van jullie het al hebben opgemerkt dat wanneer gretige laadmethoden gebruiken DOET MEE
, ze gebruiken alleen LINKER BUITEN DOE MEE
. Ook laden ze in elk geval te veel onnodige gegevens in het geheugen - ze selecteren elke afzonderlijke kolom uit de tabel, terwijl we alleen de naam van de auteur nodig hebben.
Hoewel Active Record je toestaat om voorwaarden aan de gretig geladen associaties op te geven, zoalsdoet mee()
, de aanbevolen manier is om in plaats daarvan joins te gebruiken. ~ Rails-documentatie.
Zoals aanbevolen in de documentatie over rails, de doet mee()
methode is in deze situaties een stap vooruit. Het voegt de bijbehorende tabel samen, maar laadt alleen de vereiste modelgegevens in het geheugen zoals posts in ons geval. Daarom laden we onnodige gegevens niet onnodig in het geheugen, hoewel we dit ook kunnen doen als we dat willen.
# Bestel berichten op auteursnaam. def index @posts = Post.order ("authors.name"). joins (: auteur) einde
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECTEER "berichten". * VAN "berichten" BINNEN JOIN "auteurs" AAN "auteurs". "Id" = "berichten". "Auteur_id" ORDER BY authors.name SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs" . "id" =? LIMIET 1 [["id", 2]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 1]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 3]]
# Zoek alleen berichten van de auteur "John". def index @posts = Post.order (published_at:: desc) .joins (: author) .where ("authors.name =?", "John") end
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECTEER "berichten" * FROM "berichten" INNER JOIN "auteurs" AAN "auteurs". "Id" = "berichten". "Auteur_id" WHERE (authors.name = 'Manish') BESTELLING OP "berichten". "Created_at" DESC SELECT "authors". * FROM "authors" WHERE "authors". "Id" =? LIMIET 1 [["id", 3]]
def index @posts = Post.order (published_at:: desc). joins (: auteur) eindigen
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT "posts". * FROM "posts" INNER JOIN "auteurs" AAN "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC SELECT "authors". * FROM "auteurs "WHERE" authors "." Id "=? LIMIET 1 [["id", 3]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 2]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 1]]
Het eerste dat u uit de bovenstaande resultaten kunt opmerken is dat de N + 1
probleem is terug, maar laten we eerst focussen op het goede deel.
Laten we de eerste query van alle resultaten bekijken. Ze zien er allemaal ongeveer zo uit.
SELECT "berichten". * FROM "berichten" INNER JOIN "auteurs" AAN "auteurs". "Id" = "berichten". "Auteur_id" BESTELLING BY authors.name
Het haalt alle kolommen uit berichten. Het voegt netjes beide tabellen samen en sorteert of filtert de records afhankelijk van de voorwaarde, maar zonder dat er gegevens uit de bijbehorende tabel worden opgehaald. Dat is wat we in de eerste plaats wilden.
Maar na de eerste vragen zullen we het zien 1
of 3
of N
aantal zoekopdrachten afhankelijk van de gegevens in uw database, zoals dit:
SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 2]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 1]] SELECTEER "auteurs". * VAN "auteurs" WAAR "auteurs". "Id" =? LIMIET 1 [["id", 3]]
Nu zou je je kunnen afvragen: waarom is dit N + 1
probleem terug? Het is vanwege deze lijn in onze ogen post.author.name
.
<% @posts.each do |post| %><% end %> <%= post.title %> <%= post.body %> <%= post.author.name %>
Deze regel activeert al die vragen. Dus in het voorbeeld waarin we alleen onze posts moesten bestellen, hoeven we de auteursnaam niet in onze views weer te geven. In dat geval kunnen we dit probleem oplossen door de regel te verwijderen post.author.name
van het uitzicht.
Maar dan kun je je misschien afvragen: "Hé MK, hoe zit het met de voorbeelden waarin we de naam van de auteur in de weergave willen weergeven?"
Welnu, in dat geval, de doet mee()
methode lost het niet zelf op. We zullen het moeten vertellen doet mee()
om de naam van de auteur te selecteren, of een andere kolom uit de tabel voor die kwestie. En we kunnen het doen door een select ()
verklaring aan het eind, zoals dit:
def index @posts = Post.order (published_at:: desc) .joins (: auteur) .select ("posts. *, authors.name as author_name") einde
Ik heb een alias "author_name" gemaakt voor authors.name. We zullen zien waarom in net een seconde.
Resulterende zoekopdracht in de ontwikkelingslogboeken:
SELECT posts. *, Authors.name as author_name FROM "posts" INNER JOIN "authors" OP "authors". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC
Hier gaan we: eindelijk een schone SQL-query zonder nummer N + 1
probleem, zonder onnodige gegevens, met alleen de dingen die we nodig hebben. Het enige dat overblijft is om die alias te gebruiken in jouw mening en verandering post.author.name
naar post.author_name
. Dit komt omdat de naam van de auteur nu een attribuut is van ons Post-model en na deze wijziging ziet de pagina er zo uit:
Alles precies hetzelfde, maar onder de motorkap zijn veel dingen veranderd. Als ik alles in een notendop zet, om het op te lossen N + 1
je zou moeten gaan voor gretig laden, maar af en toe, afhankelijk van de situatie, moet u dingen in uw controle en gebruik nemen doet mee voor betere opties. U kunt ook onbewerkte SQL-query's aan de server leveren doet mee()
methode voor meer maatwerk.
Meedoen en gretig laden laten ook het laden van meerdere associaties toe, maar in het begin kunnen dingen heel gecompliceerd en moeilijk worden om de beste optie te bepalen. In dergelijke situaties raad ik u aan om deze twee zeer aardige Envato Tuts + -handleidingen te lezen om meer inzicht te krijgen in joins en om de minst dure aanpak te kiezen in termen van prestaties:
Last but not least, kan het ingewikkeld zijn om gebieden in uw pre-build toepassing te vinden, waar u de prestaties in het algemeen zou moeten verbeteren of de N + 1
problemen. In die gevallen beveel ik een mooie edelsteen aan Kogel. Het kan u waarschuwen wanneer u enthousiast wilt laden N + 1
vragen en wanneer u onnodig veel laadt.