Modes de chargement des données via le DbContext
Présentation
Entity Framework (Core) est l’ORM (Object-Relational Mapper) proposé par Microsoft pour l’écosystème .NET. Il permet de manipuler les données d’une base relationnelle sous forme d’objets, en masquant une partie de la complexité liée aux requêtes SQL, aux jointures et au suivi des modifications.
Pour cela, Entity Framework s’appuie notamment sur un contexte de données, représenté par la classe DbContext. Ce contexte permet d’interroger la base, de matérialiser les lignes des tables sous forme d’objets, de suivre les changements effectués sur ces objets, puis d'enregistrer ces modifications dans la base de données. Le tracking des objets peut être désactivé pour améliorer les performances, s'il n'est pas nécessaire de suivre les modifications.
Le chargement d’un volume important de données, peut entraîner la création d’un grand nombre d’objets en mémoire. Si ces données sont suivies par le DbContext, cela peut augmenter la consommation mémoire et dégrader les performances. Il est donc important de choisir une stratégie de chargement adaptée.
L’une des solutions permettant d'améliorer les performances est de gérer le chargement de données en fonction des besoins. Pour ce faire, Entity Framework propose 3 modes de chargement des objets dans les contextes de données :
-
Eager Loading : le chargement des données associées est réalisé lors de l’exécution de la requête initiale
-
Lazy Loading : le chargement des données associées est réalisé de façon transparente lors de l’accès aux propriétés de navigation
-
Explicit Loading : le chargement des données associées est réalisé de manière explicite
A ces 3 modes, on peut ajouter deux modes liés à l’utilisation de concepts du langage C# :
-
Deferred Loading : exécution différée des requêtes
-
Partial Loading : chargement partiel des données des entités
Présentation des entités
Pour illustrer chaque manière de charger les données, nous utiliserons deux entités liées entre elles par une relation d’association : Blog et Post, où un blog contient un ensemble de posts et un post n’est rattaché qu’à un seul blog. Voici l’implémentation de ces entités :
public class Blog
{
public int Id { get; set; }
public string Name { get; set; } = "";
// virtual : nécessaire pour le lazy loading par proxy
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}
public class Post
{
public int Id { get; set; }
public string Title { get; set; } = "";
public bool IsPublished { get; set; }
public DateTime PublishedAt { get; set; }
public int BlogId { get; set; }
// virtual : nécessaire pour lazy loading par proxy
public virtual Blog Blog { get; set; } = null!;
}
Ces deux entités sont utilisées pour définir des jeux d’entités dans le DbContext et la redéfinition de la méthode d’extension OnModelCreating permet d'implémenter les relations entre les entités :
public class AppDbContext : DbContext
{
public DbSet<Blog> Blogs => Set<Blog>();
public DbSet<Post> Posts => Set<Post>();
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer("...");
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasMany(b => b.Posts)
.WithOne(p => p.Blog)
.HasForeignKey(p => p.BlogId);
}
}
Dans chaque exemple de code présenté ci-dessous, la variable db représente une instance de la classe AppDbContext :
Eager loading
Les méthodes d’extension Include et ThenInclude, accessibles sur les jeux d’entités en important l’espace de nom Microsoft.EntityFrameworkCore, permettent d’indiquer au contexte de données, les données à charger dans les instructions de requêtage. Par exemple, cette instruction charge toutes les données des blogs et leurs posts dans la variable blogs :
Attention : si trop d’instructions Include / ThenInclude sont utilisées, et / ou les propriétés de navigation sont des ensemble de données volumineux, alors il y aura un impact négatif sur les performances (cela dépend aussi de la manière les données sont sélectionnées dans la méthode Select).
Lazy loading
Le mode lazy loading permet de charger les données liées uniquement lorsqu’on accède à la propriété de navigation. Pour ce faire, il est nécessaire d’utiliser la méthode d’extension UseLazyLoadingProxies() (nécessite l'ajout du package nuget Microsoft.EntityFrameworkCore.Proxies) lors de la construction du contexte de données :
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseLazyLoadingProxies()
.UseSqlServer("...");
}
Les propriétés de navigation doivent alors être virtuelle, et les classes doivent pouvoir être héritées (autrement dit, ne pas être scellées). Voici un exemple de requête :
A ce moment, les données des posts ne sont pas encore chargées. Le parcours des données demandera le chargement des posts de chaque blog. Ainsi, si j’ai 10 blogs, 11 requêtes SQL seront envoyées vers la base de données. Une pour obtenir la liste des blogs. Dix autres pour obtenir la liste des posts de chaque blog :
Explicit Loading
Le mode Explicit Loading consiste à charger les données des blogs dans un premier temps, puis à charger explicitement le contenu des propriétés de navigation via les méthodes Load() et LoadAsync(). Voici un exemple :
var blog = await db.Blogs
.SingleAsync(b => b.Id == 1);
await db.Entry(blog)
.Collection(b => b.Posts)
.LoadAsync();
foreach (var post in blog.Posts)
Console.WriteLine(post.Title);
Les données des posts de chaque blog ne sont pas automatiquement chargées. Elles le seront lorsque l’application les demandera. Charger les données de tous les posts peut être coûteux. Si nous souhaitons obtenir uniquement les posts qui ont été publiés, nous pouvons appliquer un filtre à la requête :
var blog = await db.Blogs
.SingleAsync(b => b.Id == 1);
await db.Entry(blog)
.Collection(b => b.Posts)
.Query()
.Where(p => p.IsPublished)
.LoadAsync();
Deferred Loading
Dans le contexte LINQ / Entity Framework, construire une requête ne l’exécute pas immédiatement. Une requête est envoyée vers la base de données lorsque les données sont demandées, par exemple avec l'utilisation des méthodes ToListAsync(), SingleAsync(), CountAsync() ou une itération sur le résultat de la requête. Ainsi, l’exécution de la requête :
ne provoque l’envoi d’aucune requête SQL vers la base de données. Pour l’exécuter, il est nécessaire de demander des données. Par exemple :
Partial Loading
Le mode Partial Loading n’est pas un mode de chargement des données proposé par Entity Framework. Via une projection de données implémentée avec des types anonymes ou des DTO (Data Transfer Object). Voici un exemple avec les DTO suivants :
public sealed record BlogSummaryDto(int Id, string Name, List<PostSummaryDto> Posts);
public sealed record PostSummaryDto(int Id, string Title);
Une requête LINQ permet d’obtenir uniquement l’identifiant et le nom des blogs, ainsi que l’identifiant et le titre des trois premiers posts les plus récemment publiés pour chacun d'entre eux :