Les requĂȘtes N+1 sont une plaie qui peuvent avoir un impact Ă©norme sur les performances d'une application. Ce phĂ©nomĂšne se produit souvent lorsqu'on utilise un ORM.
T'inquiĂštes pas, je mâoccupe de tout.
Active Record de Ruby on Rails
L'ORM gĂ©nĂšre automatiquement les requĂȘtes SQL et nous Ă©vite ainsi de les taper Ă la main. Cet outil bien pratique est Ă double tranchant car il peut gĂ©nĂ©rer un grand nombre de requĂȘte SQL.
Exemple
Voici un exemple oĂč l'on veut rĂ©cupĂ©rer tous les utilisateurs qui ont dĂ©jĂ crĂ©Ă© une recette. Sans rĂ©flĂ©chir, on serait tentĂ© de faire plus ou moins comme ça:
users = Recipe.all.map{|recipe| recipe.user}
# SELECT "recipes".* FROM "recipes"
# SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
# SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 2]]
On voit ici qu'on gĂ©nĂšre trois requĂȘtes.
Recipe.all
= 1 requĂȘte pour rĂ©cupĂ©rer les recettesrecipe.user
= 1 requĂȘteSELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT 1 [["id", 1]]
par recette
Donc, si nous avons 1000 recettes en base, nous gĂ©nĂ©rons 1001 requĂȘtes SQL.
Comment corriger?
C'est lĂ qu'intervient includes
. includes
va prĂ©-charger les objets enfants dans une seule requĂȘte. Son utilisation est trĂšs facile. Si on reprend l'exemple prĂ©cĂ©dent
users = Recipe.includes(:user).all.map{|recipe| recipe.user}
# SELECT "recipes".* FROM "recipes"
# SELECT "users".* FROM "users" WHERE "users"."id" IN (?, ?) [["id", 1],["id", 2]]
Et voilĂ :
Recipe.all
= 1 requĂȘteSELECT "recipes".* FROM "recipes"
recipe.user
= 1 requĂȘteSELECT "users".* FROM "users" WHERE "users"."id" IN (1, 2)
includes
ne s'arrĂȘte pas lĂ et nous permet mĂȘme de prĂ©charger les sous-liaisons trĂšs facilement:
Recipe.includes(comments: :user)
Active Record est magique!
Comment prévenir?
Nous avons vu qu'il est malheureusement trĂšs facile de crĂ©er une requĂȘte N+1. Heureusement, il existe une gem qui permet de nous alerter lorsque ce genre de situation arrive: Bullet. Bullet va nous prĂ©venir (par mail, notification growl, console, etc..) lorsquâil trouve une requĂȘte N+1.
Pour l'installer, on ajoute la gem au GemFile
# Gemfile
group :development do
gem 'bullet'
end
On n'oublie pas de lancer un bundle install
pour installer la dépendance et on crée un initializer pour configurer Bullet.
# config/initializers/bullet.rb
Rails.application.config.after_initialize do
Bullet.enable = true
Bullet.alert = true
end
Changer la configuration nécessite le redémarrage du serveur
Ici j'ai seulement activé une window.alert
JavaScript. Il suffit ensuite d'utiliser l'application normalement et lorsque l'on rencontrera une requĂȘte N+1: