Aller au contenu

Exécuter les requêtes au bon moment

Introduction

Avec Entity Framework Core, les requêtes Linq to Entities sont généralement construites de manière différée. Tant qu’une requête est exposée au travers du type IQueryable<T>, les méthodes telles que Where, Select, OrderBy, etc. n'entraîne pas l'exécution de la requête SQL. Ils enrichissent progressivement une requête qui sera ensuite traduite en SQL et exécutée par le serveur de base de données. En revanche, d'autres méthodes telles que ToList, ToListAsync, ToArray, ToArrayAsync, First, FirstAsync, FirstOrDefault, FirstOrDefaultAsync, Count, CountAsync, Any, AnyAsync, etc. déclenchent immédiatement l’exécution de la requête.

Exécuter une requête trop tôt peut conduire à charger en mémoire plus de données que nécessaire. Les filtres, tris ou projections ajoutés ensuite sont alors exécutés par l’application sur les données stockées en mémoire, au lieu de l’être par le serveur de base de données.

Problématique

Dans une application, nous devons afficher des informations sur des clients dont le nom commence par la lettre D. Nous pourrions écrire :

List<Customer> customers = await context.Customer.ToListAsync();

List<Customer> filteredCustomers = customers
    .Where(c => c.LastName.StartsWith("D"))
    .ToList();

Avec l’utilisation de la méthode ToListAsync(), la requête SQL est immédiatement exécutée. L’ensemble des lignes et des colonnes de la table Customer sont donc obtenues depuis la base de données et stocker en mémoire. La méthode Where est ensuite appliquée sur une collection de type List<Customer>, et filtre les données stockées en mémoire. Si la table contient un volume important de données, cette approche augmente inutilement le volume de données obtenues entre la base et le PC qui exécute l'application, et pénalise les performances de l'application.

Conserver la requête via l'interface IQueryable<T>

Il est préférable de construire la requête Linq To Entities dans un premier temps, avant de l’exécuter dans un second temps :

IQueryable<Customer> query = context.Customer
    .Where(c => c.LastName.StartsWith("D"))
    .OrderBy(c => c.LastName);

List<Customer> customers = await query.ToListAsync();

Dans cet exemple de code, EF Core filtre et tri les données via une requête SQL. Ainsi, le serveur de la base de données retourne uniquement les clients filtrés et triés. L’utilisation d'un objet de type IQueryable<Customer> est intéressante lorsque la requête doit être enrichie progressivement, par exemple selon des critères de filtre :

IQueryable<Customer> query = context.Customers;

if (!string.IsNullOrWhiteSpace(searchTerm))
    query = query.Where(c => c.LastName.StartsWith(searchTerm));

if (onlyActiveCustomers)
    query = query.Where(c => c.IsActive);

List<Customer> customers = await query
    .OrderBy(c => c.LastName)
    .ToListAsync();

La requête n’est exécutée qu’au moment de l’appel à la méthode ToListAsync, une fois tous les critères de filtre appliqués.

Attention à l'utilisation de la méthode AsEnumerable

La méthode AsEnumerable ne déclenche pas immédiatement l’exécution de la requête. En revanche, elle fait basculer la suite des traitements vers Linq to Objects : les opérateurs placés après cet appel ne seront plus traduits en SQL, mais s'exécuteront sur les données obtenues en mémoire :

List<Customer> customers = await context.Customers
    .AsEnumerable()
    .Where(c => c.LastName.StartsWith("D"))
    .ToListAsync();

Il est donc nécessaire de réserver l'utilisation de cette méthode aux situations particulières où un traitement ne peut réellement pas être traduit en SQL, et l’utiliser le plus tard possible dans la requête.

Comment améliorer les performances ?

Pour éviter d’exécuter une requête trop tôt :

  • Conservons la requête sous la forme d’un IQueryable<T> pendant sa construction.

  • Appliquons les filtres, tris et projections avant l'utilisation d'une méthode exécutant la requête. Ainsi, par exemple, n'utilisons la méthode ToListAsync uniquement lorsque les données doivent être récupérées.

  • Appliquons la méthode AsEnumerable après les opérations pouvant être traduites en SQL.

  • Projetons les données via un type anonyme ou un DTO (voir cet article) :

List<CustomerDto> customers = await context.Customers
    .Where(c => c.LastName.StartsWith("D"))
    .OrderBy(c => c.LastName)
    .Select(c => new CustomerDto(
        c.Id,
        c.FirstName,
        c.LastName))
    .ToListAsync();

Cette requête Linq To Entities cumule deux optimisations : le filtrage et le tri sont pris en charge par le serveur de base de données, tandis que seules les colonnes demandées sont obtenues.