Les utilisateurs aiment les applications rapides, ils tombent amoureux d'eux et en font une partie de leur vie. Les applications lentes, d’autre part, agacent uniquement les utilisateurs et perdent des revenus. Dans ce didacticiel, nous allons nous assurer de ne pas perdre plus d’argent ou d’utilisateurs, et de comprendre les différentes manières d’améliorer les performances..
Active Records et ORM sont des outils très puissants dans Ruby on Rails, mais uniquement si nous savons comment libérer et utiliser ce pouvoir. Au début, vous trouverez une multitude de façons d'effectuer une tâche similaire dans RoR.,mais ce n'est que lorsque vous creusez un peu plus profond que vous apprenez réellement à connaître les coûts d'utilisation d'un sur l'autre.
Il en va de même pour ORM et Associations in Rails. Ils nous rendent la vie beaucoup plus facile, mais dans certaines situations, ils peuvent aussi être excessifs.
Mais avant cela, générons rapidement une application factice pour jouer avec.
Démarrez votre terminal et tapez ces commandes pour créer une nouvelle application:
rails nouveau blog cd blog
Générez votre application:
rails g échafaudage Nom de l'auteur: chaîne rails g échafaudage Titre de l'article: chaîne de caractères: texte auteur: références
Déployez-le sur votre serveur local:
rake db: migrate rails s
Et c'était ça! Maintenant, vous devriez avoir une application factice en cours d'exécution.
Voici à quoi devraient ressembler nos deux modèles (Auteur et Poste). Nous avons des publications qui appartiennent à l'auteur, et nous avons des auteurs qui peuvent avoir de nombreuses publications. C’est la relation / association de base entre ces deux modèles avec laquelle nous allons jouer..
# Post Modèle Post Classe < ActiveRecord::Base belongs_to :author end # Author Model class Author < ActiveRecord::Base has_many :posts end
Jetez un coup d'œil à votre "Contrôleur de postes" - voici à quoi cela devrait ressembler. Nous nous concentrerons principalement sur sa méthode d'indexation..
# Contrôleur de classe PostsController < ApplicationController def index @posts = Post.order(created_at: :desc) end end
Et enfin, notre vue Index des publications. Il se peut que vos lignes aient des lignes supplémentaires, mais ce sont celles-là sur lesquelles je veux que vous vous concentriez, en particulier la ligne avec post.author.name
.
<% @posts.each do |post| %><% end %> <%= post.title %> <%= post.body %> <%= post.author.name %>
Créons simplement des données factices avant de commencer. Allez dans votre console de rails et ajoutez les lignes suivantes. Ou vous pouvez simplement aller à http: // localhost: 3000 / posts / new
et http: // localhost: 3000 / authors / new
ajouter des données manuellement.
authors = Author.create ([name: 'John', name: 'Doe', name: 'Manish'])) Post.create (titre: 'I love Tuts +', corps: ", auteur: authors.first) Post.create (titre: 'Tuts + is Awesome', corps: ", auteur: authors.second) Post.create (titre: 'Long Live Tuts +', corps:", author: authors.last)
Maintenant que vous êtes tous configurés, démarrons le serveur avec rails s
et frapper localhost: 3000 / posts
.
Vous verrez des résultats sur votre écran comme ceci.
Donc, tout semble aller pour le mieux: pas d’erreurs, et il récupère tous les enregistrements avec les noms d’auteurs associés. Mais si vous regardez votre journal de développement, vous verrez des tonnes de requêtes en cours d'exécution comme ci-dessous..
Post Load (0.6ms) SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC Auteur Charge (0.5ms) SELECT "auteurs". * FROM "auteurs" WHERE "auteurs". "Id" =? LIMIT 1 [["" id ", 3]] Author Load (0.1ms) SELECT les" auteurs ". * FROM" authors "WHERE" authors "." Id "=? LIMIT 1 [["" id ", 2]] Author Load (0.1ms) SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMITE 1 [["" id ", 1]]
Bien, d'accord, je suis d'accord qu'il ne s'agit que de quatre requêtes, mais imaginez que votre base de données contienne 3 000 messages au lieu de trois. Dans ce cas, notre base de données sera inondée de 3 000 + 1 requêtes, raison pour laquelle ce problème est appelé le N + 1
problème.
Ainsi, par défaut, dans Ruby on Rails, le chargement différé est activé dans l'ORM, ce qui signifie qu'il retarde le chargement des données jusqu'au point où nous en avons réellement besoin..
Dans notre cas, c'est d'abord le contrôleur où il est demandé de récupérer tous les posts.
def index @posts = Post.order (created_at:: desc) end
La deuxième est la vue, où nous parcourons les publications extraites par le contrôleur et envoyons une requête pour obtenir le nom de l'auteur de chaque publication séparément. D'où le N + 1
problème.
<% @posts.each do |post| %>… <% end %><%= post.author.name %>
Pour nous sauver de telles situations, Rails nous propose une fonctionnalité appelée chargement rapide.
Le chargement rapide vous permet de précharger les données associées (auteurs)pour tous les des postes à partir de la base de données, améliore les performances globales en réduisant le nombre de requêtes et vous fournit les données que vous souhaitez afficher dans vos vues, mais le seul inconvénient ici est celle à utiliser. Je t'ai eu!
Oui, parce que nous en avons trois et qu’ils servent tous le même objectif, mais selon les cas, chacun d’entre eux peut à nouveau réduire ou surexploiter la performance..
preload () eager_load () includes ()
Maintenant, vous pourriez demander lequel utiliser dans ce cas? Eh bien, commençons par le premier.
def index @posts = Post.order (created_at:: desc) .preload (: author) end
Sauvegarde le. Appuyez à nouveau sur l'URL localhost: 3000 / posts
.
Donc, aucun changement dans les résultats: tout se charge exactement de la même façon, mais sous le capot du journal de développement, ces tonnes de requêtes ont été modifiées pour les deux suivantes.
SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "auteurs". * FROM "auteurs" WHERE "auteurs". "Id" IN (3, 2, 1)
Le préchargement utilise deux requêtes distinctes pour charger les données principales et les données associées. C’est bien mieux que d’avoir une requête distincte pour chaque nom d’auteur (le problème N + 1), mais cela ne nous suffit pas. En raison de son approche de requêtes séparée, il lancera une exception dans les scénarios suivants:
# Commandez les publications par nom d’auteur. def index @posts = Post.order ("authors.name"). eager_load (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". "Id" AS t0_r0, "posts". "Titre" AS t0_r1, "posts". "Corps" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "auteurs". "id" AS t1_r0, "auteurs". "nom" AS t1_r1, "auteurs". "created_at" AS t1_r2, "auteurs". "updated_at" AS_1_r3 FROM "posts" LEFT OUTER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY authors.name
# Trouver les messages de l'auteur "John" uniquement. def index @posts = Post.order (created_at:: desc) .eager_load (: author) .where ("authors.name =?", "Manish") end
Requête résultante dans les journaux de développement:
SELECT "posts". "Id" AS t0_r0, "posts". "Titre" AS t0_r1, "posts". "Corps" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "auteurs". "id" AS t1_r0, "auteurs". "nom" AS t1_r1, "auteurs". "created_at" AS t1_r2, "auteurs". "updated_at" AS_1_r3 FROM "posts" LEFT OUTER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC
def index @posts = Post.order (created_at:: desc) .eager_load (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". "Id" AS t0_r0, "posts". "Titre" AS t0_r1, "posts". "Corps" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "auteurs". "id" AS t1_r0, "auteurs". "nom" AS t1_r1, "auteurs". "created_at" AS t1_r2, "auteurs". "updated_at" AS_1_r3 FROM "posts" LEFT OUTER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC
Donc, si vous regardez les requêtes résultantes des trois scénarios, il y a deux choses en commun.
Premier, eager_load ()
utilise toujours le JOINTURE EXTERNE GAUCHE
quel que soit le cas. Deuxièmement, il obtient toutes les données associées dans une seule requête, ce qui dépasse certainement le précharge ()
méthode dans les situations où nous voulons utiliser les données associées pour des tâches supplémentaires telles que la commande et le filtrage. Mais une seule requête et JOINTURE EXTERNE GAUCHE
Cela peut aussi être très coûteux dans des scénarios simples comme ci-dessus, où tout ce dont vous avez besoin est de filtrer les auteurs souhaités. C'est comme utiliser un bazooka pour tuer une petite mouche.
Je comprends que ce ne sont que deux exemples simples et que, dans des scénarios réels, il peut être très difficile de choisir celui qui convient le mieux à votre situation. C’est la raison pour laquelle Rails nous a donné la comprend ()
méthode.
Avec comprend ()
, Active Record prend en charge la décision difficile. Il est bien plus intelligent que le précharge ()
et eager_load ()
méthodes et décide lequel utiliser seul.
# Commandez les publications par nom d’auteur. def index @posts = Post.order ("authors.name"). inclut (: author) fin
Requête résultante dans les journaux de développement:
SELECT "posts". "Id" AS t0_r0, "posts". "Titre" AS t0_r1, "posts". "Corps" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "auteurs". "id" AS t1_r0, "auteurs". "nom" AS t1_r1, "auteurs". "created_at" AS t1_r2, "auteurs". "updated_at" AS_1_r3 FROM "posts" LEFT OUTER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY authors.name
# Trouver les messages de l'auteur "John" uniquement. def index @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish") # Pour les rails 4 N'oubliez pas d'ajouter .references (: author ) à la fin @posts = Post.order (created_at:: desc) .includes (: author) .where ("authors.name =?", "Manish"). references (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". "Id" AS t0_r0, "posts". "Titre" AS t0_r1, "posts". "Corps" AS t0_r2, "posts". "Author_id" AS t0_r3, "posts". "Created_at" AS t0_r4 , "posts". "updated_at" AS t0_r5, "auteurs". "id" AS t1_r0, "auteurs". "nom" AS t1_r1, "auteurs". "created_at" AS t1_r2, "auteurs". "updated_at" AS_1_r3 FROM "posts" LEFT OUTER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC
def index @posts = Post.order (created_at:: desc) .includes (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". * FROM "posts" ORDER BY "posts". "Created_at" DESC SELECT "auteurs". * FROM "auteurs" WHERE "auteurs". "Id" IN (3, 2, 1)
Maintenant, si on compare les résultats avec les eager_load ()
méthode, les deux premiers cas ont des résultats similaires, mais dans le dernier cas, il a intelligemment décidé de passer au précharge ()
méthode pour une meilleure performance.
Non, car dans cette course à la performance, un chargement parfois enthousiaste peut également échouer. J'espère que certains d'entre vous ont déjà remarqué que chaque fois que des méthodes de chargement rapides sont utilisées Se joint
, ils utilisent seulement JOINTURE EXTERNE GAUCHE
. De plus, dans tous les cas, ils chargent trop de données inutiles dans la mémoire: ils sélectionnent chaque colonne de la table, alors que nous n'avons besoin que du nom de l'auteur..
Même si Active Record vous permet de spécifier des conditions sur les associations chargées avec impatience, tout commerejoint ()
, la méthode recommandée consiste à utiliser des jointures à la place. ~ Documentation Rails.
Comme recommandé dans la documentation de rails, le rejoint ()
La méthode a une longueur d’avance dans ces situations. Il rejoint la table associée, mais ne charge que les données de modèle requises en mémoire, comme des postes dans notre cas. Par conséquent, nous ne chargeons pas inutilement des données redondantes dans la mémoire, même si nous pouvons le faire aussi..
# Commandez les publications par nom d’auteur. def index @posts = Post.order ("authors.name"). joins (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". * FROM "posts" INNER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY authors.name SELECT "auteurs". * FROM "auteurs" WHERE "auteurs" "id" =? LIMIT 1 [["" id ", 2]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMIT 1 [["" id ", 1]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMITE 1 [["" id ", 3]]
# Trouver les messages de l'auteur "John" uniquement. def index @posts = Post.order (published_at:: desc) .joins (: author) .where ("authors.name =?", "John") end
Requête résultante dans les journaux de développement:
SELECT "posts". * FROM "posts" INNER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" WHERE (authors.name = 'Manish') ORDER BY "posts". "Created_at" DESC SELECT "auteurs". * FROM "auteurs" WHERE "auteurs". "Id" =? LIMITE 1 [["" id ", 3]]
def index @posts = Post.order (published_at:: desc) .joins (: author) end
Requête résultante dans les journaux de développement:
SELECT "posts". * FROM "posts" INNER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC SELECT "auteurs". * FROM "auteurs "WHERE" auteurs "." Id "=? LIMIT 1 [["" id ", 3]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMIT 1 [["" id ", 2]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMITE 1 [["" id ", 1]]
La première chose que vous remarquerez des résultats ci-dessus est que le N + 1
le problème est de retour, mais concentrons-nous d'abord sur la bonne partie.
Regardons dans la première requête de tous les résultats. Tous ressemblent plus ou moins à ça.
SELECT "posts". * FROM "posts" INNER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY authors.name
Il récupère toutes les colonnes des posts. Il joint joliment les deux tables et trie ou filtre les enregistrements en fonction de la condition, mais sans extraire aucune donnée de la table associée. Quel est ce que nous voulions en premier lieu.
Mais après les premières requêtes, nous verrons 1
ou 3
ou N
nombre de requêtes en fonction des données de votre base de données, comme ceci:
SELECT "auteurs". * FROM "auteurs" WHERE "auteurs". "Id" =? LIMIT 1 [["" id ", 2]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMIT 1 [["" id ", 1]] SELECT les" auteurs ". * FROM" auteurs "WHERE" auteurs "." Id "=? LIMITE 1 [["" id ", 3]]
Maintenant, vous pourriez demander: pourquoi est-ce N + 1
problème de retour? C'est à cause de cette ligne à notre avis post.author.name
.
<% @posts.each do |post| %><% end %> <%= post.title %> <%= post.body %> <%= post.author.name %>
Cette ligne déclenche toutes ces requêtes. Ainsi, dans l'exemple où nous n'avions qu'à commander nos articles, nous n'avons pas besoin d'afficher le nom de l'auteur dans nos vues. Dans ce cas, nous pouvons résoudre ce problème en supprimant la ligne post.author.name
de la vue.
Mais ensuite, vous pourriez demander: "Hey MK, qu'en est-il des exemples où nous voulons afficher le nom de l'auteur dans la vue?"
Eh bien, dans ce cas, le rejoint ()
méthode ne va pas le réparer par lui-même. Nous devrons dire rejoint ()
pour sélectionner le nom de l'auteur ou n'importe quelle autre colonne de la table. Et nous pouvons le faire en ajoutant un sélectionner()
déclaration à la fin, comme ceci:
def index @posts = Post.order (published_at:: desc) .joins (: author) .select ("posts. *, authors.name en tant que nom_auteur") end
J'ai créé un alias "author_name" pour authors.name. Nous verrons pourquoi dans juste une seconde.
Requête résultante dans les journaux de développement:
SELECT posts. *, Auteurs.nom en tant qu'auteur_nom FROM "posts" INNER JOIN "auteurs" ON "auteurs". "Id" = "posts". "Author_id" ORDER BY "posts". "Created_at" DESC
On y va: enfin une requête SQL propre sans N + 1
problème, sans données inutiles, avec juste ce dont nous avons besoin. La seule chose qui reste à faire est d’utiliser cet alias dans votre vue et de le modifier. post.author.name
à post.author_name
. En effet, nom_auteur est maintenant un attribut de notre modèle Post et, après ce changement, voici à quoi ressemble la page:
Tout est exactement pareil, mais sous le capot beaucoup de choses ont été changées. Si je résume le tout, pour résoudre le problème N + 1
vous devriez aller chercher chargement rapide, mais parfois, en fonction de la situation, vous devez prendre les choses en main et les utiliser se joint pour de meilleures options. Vous pouvez également fournir des requêtes SQL brutes au rejoint ()
méthode pour plus de personnalisation.
Les jointures et le chargement rapide permettent également le chargement de multiples associations, mais au début, les choses peuvent devenir très compliquées et difficiles à choisir. Dans de telles situations, je vous recommande de lire ces deux très bons tutoriels Envato Tuts + pour mieux comprendre les jointures et pouvoir choisir l'approche la moins chère en termes de performances:
Dernier point mais non le moindre, il peut être compliqué de déterminer les zones de votre application de pré-construction dans lesquelles vous devriez améliorer les performances ou rechercher le N + 1
problèmes. Dans ces cas, je recommande un bon bijou appelé Balle. Il peut vous avertir quand vous devez ajouter un chargement rapide pour N + 1
requêtes, et lorsque vous utilisez inutilement le chargement inutilement.