Gagner en performance en évitant les requêtes N+1
En quoi les requêtes N+1 sont elles néfastes pour les applications ?
Les requêtes N+1 sont un problème récurant dans les applications qui utilisent des bases de données avec des schémas complexes. Elles surviennent lorsqu'une requête initiale entraîne d'autres requêtes supplémentaires pour récupérer des données associées.
Par exemple, le lazy loading d'Eloquent récupère automatiquement des informations en base de données lorsque cela est nécessaire.
Bien que cela puisse être pratique dans certains cas, dans un contexte non maîtrisé, cela peut fortement solliciter vos bases de données, voire les submerger 🌊
Dans cet article, nous allons explorer quelques méthodes pour éviter ce problème et optimiser les performances de vos applications.
Comment détecter les requêtes en doublon ?
Des outils de débogage comme Telescope ou la Debugbar peuvent vous aider à analyser des requêtes existantes.
Dans notre exemple, nous allons utiliser Laravel Debugbar pour surveiller les requêtes.
Le code suivant va générer des requêtes superflues, car la vue affiche des données de la relation artitsts
non présentes dans la requête initiale :
1// AlbumController.php 2 3function showAlbums() 4{ 5 $albums = Album::all(); 6 7 return view('albums', ['albums' => $albums]); 8} 9 10// albums.blade.php11 12@foreach($albums as $album)13 14 {{ $album->artist->name }}15 16@endforeach
Comme on peut le constater dans la Debugbar, le code précédent génère des doublons de requêtes à chaque iteration d'album :
Comment corriger ce problème?
Le cas typique est une relation chargée au sein d'une boucle issue d'une requête transmise depuis un controller.
Pour résoudre ce problème, il est nécessaire de charger toutes les relations pertinentes pour notre cas d'utilisation lors de la première requête en utilisant la méthode with()
:
1// AlbumController.php2 3function showAlbums()4{5 $albums = Album::with('artist')->get() 6 7 return view('albums', ['albums' => $albums]);8}
Selon le contexte, vous pouvez également utiliser la méthode load()
, qui chargera les relations manquantes de votre collection Eloquent au besoin :
1// albums.blade.php2 3@foreach($albums->load('artist') as $album) 4 5 {{ $album->artist->name }}6 7@endforeach
À l’instar de la méthode load()
, la méthode loadMissing()
permet de charger la relation seulement si elle est manquante, ce qui est pratique dans le cas où vous n'êtes pas sûr des relations précédemment chargées par Eloquent.
Voici le résultat après la modification :
Cette simple modification permet d'économiser une requête par élément présent dans la collection. N'est-ce pas magnifique ? En plus de sauver la planète en évitant d'utiliser trop de ressources, vos applications sont maintenant plus efficientes!
Mais attention ! L’accès à une relation parente peut également générer des requêtes superflues.
1// AlbumController.php 2 3$albums = Album::with('tracks')->get(); 4 5foreach ($albums as $album) 6{ 7 foreach ($album->tracks as $track) 8 { 9 dump($track->album->name); 10 }11}
En ajoutant la méthode chaperone()
à votre relation enfant, vous éviterez ce problème :
1// Album.php2 3function tracks(): HasMany4{5 return $this->hasMany(Track::class)->chaperone();6}
Maintenant que vos requêtes sont corrigées, comment anticiper les prochains doublons et les corriger au moment du développement ?
Ajoutez dans la méthode boot
de app/Providers/AppServiceProvider.php
le code suivant :
1// AppServiceProvider.php2 3Model::preventLazyLoading(! app()->isProduction());
Dorénavant, les requêtes N+1 lèveront une exception pour vous avertir lors de vos phases de développement 🔮
Le trou dans la raquette
Il est possible que, malgré tous ces efforts, des requêtes N+1 passent sous le radar📡
1// AlbumController.php2 3$albums = Album::query()4 //->with('artist:id,name')5 ->limit(1) 6 ->get();
La méthode preventLazyLoading
se déclenche seulement à la seconde requête N+1, pour cette raison il est important de tester votre code avec un jeu de données assez dense.
Mais ne vous inquiétez pas, il y a quand même une solution pour surveiller vos requêtes en production 😉
La fonction handleLazyLoadingViolationUsing
est là pour vous aider :
1// AppServiceProvider.php 2 3Model::preventLazyLoading(); 4 5Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) { 6 7 $class = $model::class; 8 9 info("Attempted to lazy load [{$relation}] on model [{$class}].");10 11});
Cette méthode permet de déclencher une logique lorsqu'une requête N+1 est détectée. Dans notre exemple, nous ajoutons un log, parfait pour surveiller les performances des requêtes, même en production !
⚠️ Attention, cette méthode est déclenchée à chaque requête dupliquée. Il est donc préférable de mettre en place une limite si vous voulez, par exemple, être notifié directement sur Slack ou Discord, sous peine d'être complètement submergé de messages à chaque doublon.
1// AppServiceProvider.php 2 3Model::handleLazyLoadingViolationUsing(function (Model $model, string $relation) { 4 5 $class = $model::class; 6 7 RateLimiter::attempt( 8 key: "lazy-loading:$relation-$class", 9 maxAttempts: 1,10 callback: function () {11 // Votre notification,12 },13 decaySeconds: 360014 );15 16});
En appliquant ces différents procédés, vous pouvez aisément améliorer les performances de vos applications Laravel et éviter les pièges des requêtes N+1.
A lire
Autres articles de la même catégorie
Initiation à l’analyse de code
Initiez-vous à l'analyse de code grace à phpmetrics !
Mathieu De Gracia
Que cache le cookie de session de Laravel
Analysons le cookie de session d'une application Laravel et son importance dans l'authentification
Mathieu De Gracia
Utilisez Illuminate en dehors de Laravel
Vous les utilisez de façon naturelle sans même vous en rendre compte, les bibliothèques "Illuminate" nous rendent bien des services.
Antoine Benevaut