Eviter le chargement massif de données
Introduction
Lorsqu’une requête est exécutée via Entity Framework Core, il est tentant de charger directement les entités, puis de les manipuler pour répondre aux besoins métiers. Pourtant, une entité peut contenir de nombreuses propriétés. Charger systématiquement l’ensemble de ces données pour afficher une simple liste n'est pas toujours utile et peut malheureusement dégrader les performances.
Ce problème devient particulièrement visible lorsque la table requêtée contient un nombre important de lignes ou de colonnes. L’objectif est donc de requêter uniquement les lignes et les colonnes réellement nécessaires pour répondre aux besoins métiers.
Problématique
Imaginons une table nommée Customer contenant notamment les colonnes suivantes :
public class Customer
{
public long Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Address { get; set; } = string.Empty;
public string PhoneNumber { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; } = DateTime.Now;
public string Notes { get; set; } = string.Empty;
}
Pour afficher une liste de clients, l'application a besoin d'obtenir le prénom et le nom de chacun d'eux. Nous pourrions donc écrire ces instructions :
var customers = await context.Customer.ToListAsync();
foreach (Customer customer in customers)
Console.WriteLine($"{customer.FirstName} {customer.LastName}");
Cette requête s'exécute sans erreur, les noms et prénoms s'affichent correctement, mais Entity Framework Core génère une requête SQL demandant toutes les colonnes de la table Customer :
SELECT
"Id",
"FirstName",
"LastName",
"Email",
"Address",
"PhoneNumber",
"CreatedAt",
"Notes"
FROM "Customer";
Les propriétés Email, Address, PhoneNumber, etc. sont donc donc obtenues depuis la base de données, alors qu’elles ne sont pas utilisées.
Projeter uniquement les données nécessaires
Pour éviter ce chargement de données inutile, il est préférable d’utiliser la méthode Select afin d'obtenir les données nécessaires. Cette méthode prend en paramètre une expression lambda dont le corps utilise un type anonyme, une première solution pour réaliser une projection de données :
var customers = await context.Customer
.Select(customer => new
{
customer.FirstName,
customer.LastName
})
.ToListAsync();
foreach (var customer in customers)
Console.WriteLine($"{customer.FirstName} {customer.LastName}");
La requête SQL générée ne sélectionne alors que les deux informations demandées :
La base de données transfère moins de données, l’application consomme moins de mémoire et Entity Framework Core n’a pas à matérialiser des objets de type Customer complets. C'est un levier qui permet d'améliorer les performances dans l'accès aux données.
Utiliser un DTO au lieu d'un type anonyme
Dans une application, il est généralement préférable d’utiliser un type explicite plutôt qu’un type anonyme, notamment lorsque les objets sont retournés à une méthode. Ce type explicite est caractérisé de DTO (Data Transfer Object), car son rôle est de transférer des données d'un service à l'autre au sein d'une même application ou entre applications.
public class CustomerDto
{
public string FirstName { get; init; } = string.Empty;
public string LastName { get; init; } = string.Empty;
public CustomerDto(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}
Cette classe peut être implémentée de manière simplifiée, en utilisant le constructeur principal :
public class CustomerDto(string firstName, string lastName)
{
public string FirstName { get; init; } = firstName;
public string LastName { get; init; } = lastName;
}
Ou en utilisant un record, selon les utilisations les caractéristiques souhaitées des objets obtenus :
La projection utilisant le type CustomerDto devient alors :
var customers = await context.Customers
.Select(customer => new CustomerDto (customer.FirstName, customer.LastName)))
.ToListAsync();
Cette approche rend explicite le contrat de données attendu par l’écran ou le service consommateur. La requête SQL générée sélectionne toujours et uniquement les deux informations demandées :