Gestion des Mises à Jour de Données
EF Core offre plusieurs méthodes pour modifier des enregistrements en masse dans la base de données :
- Exécution directe de SQL : Pour des mises à jour simples, vous pouvez exécuter une commande SQL brute.
ctx.Database.ExecuteSqlRaw("UPDATE [T_Books] SET [Price] = [Price] + 2");
- Mises à jour individuelles via LINQ (méthode traditionnelle) : Cette approche implique de récupérer les entités, de les modifier en mémoire, puis de sauvegarder les changements. Cela génère une requête
UPDATEdistincte pour chaque entité modifiée.
var booksToUpdate = ctx.Books.Where(b => b.Price > 10);
foreach (var book in booksToUpdate)
{
book.Price += 1;
}
await ctx.SaveChangesAsync();
- Utilisation de
ExecuteUpdate(EF Core 7.0+) : Pour des mises à jour plus efficaces, EF Core 7.0 et versions ultérieures introduisentExecuteUpdate. Cette méthode traduit directement l'opération en une seule commande SQL, optimisant les performances.
ctx.Books.ExecuteUpdate(s => s.SetProperty(e => e.Price, e => e.Price + 1000));
await ctx.SaveChangesAsync();
De même, pour la suppression en masse :
ctx.Articles.ExecuteDelete(b => b.Id > 4);
Configuration des Modèles : Annotations vs. Fluent API
EF Core propose deux approches principales pour configurer votre modèle de données :
- Annotations de Données (Data Annotations) : Les configurations sont appliquées directement sur les classes d'entités sous forme d'attributs.
[Table("T_Books")]
public class Book
{
// ... propriétés
}
- Avantages : Simplicité d'utilisation.
- Inconvénients : Couplage élevé avec le framework, moins de flexibilité.
- Fluent API : Les configurations sont définies dans des classes séparées implémentant
IEntityTypeConfiguration<TEntity>.
public class BookConfiguration : IEntityTypeConfiguration<Book>
{
public void Configure(EntityTypeBuilder<Book> builder)
{
builder.ToTable("T_Books");
// ... autres configurations
}
}
- Avantages : Découplage, flexibilité accrue.
- Inconvénients : Peut sembler plus complexe initialement.
Visualisation des Requêtes SQL Générées
Pour comprendre les requêtes SQL exécutées par EF Core, plusieurs options sont disponibles :
- Journalisation standard avec
ILoggerFactory: Configurez une fabrique de journaux pour afficher les requêtes SQL dans la console.
private static readonly ILoggerFactory loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseLoggerFactory(loggerFactory);
// ... autres configurations
}
- Journalisation simplifiée avec
LogTo: Une méthode plus directe pour journaliser les messages dans la console.
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.LogTo(msg => Console.WriteLine(msg));
// ... autres configurations
}
ToQueryString(): Cette méthode permet d'obtenir la représentation SQL d'une requêteIQueryableavant son exécution.
Gestion des Relations Complexes et des Contraintes de Clé Étrangère
Lorsqu'une entité possède plusieurs clés étrangères pointant vers la même table, des problèmes de cascade peuvent survenir. Par exemple, avec les entités Leave et User, où Leave a deux relations avec User (Requester et Approver) :
public class Leave
{
public int Id { get; set; }
public User Requester { get; set; }
public User Approver { get; set; }
// ... autres propriétés
}
public class User { /* ... */ }
// Configuration
public void Configure(EntityTypeBuilder<Leave> builder)
{
builder.ToTable("T_Leaves");
builder.HasOne<User>(a => a.Requester).WithMany().IsRequired();
builder.HasOne<User>(a => a.Approver).WithMany().OnDelete(DeleteBehavior.Restrict); // Ajout de la contrainte
}
L'erreur "FOREIGN KEY constraint ... might cause cycles or multiple cascade paths" peut être résolue en spécifiant explicitement le comportement de suppression pour l'une des relations, par exemple en utilisant OnDelete(DeleteBehavior.Restrict).
Arborescences d'Organisation avec Auto-Référencement
Pour les structures hiérarchiques comme les unités d'organisation, où une OrgUnit peut avoir un Parent OrgUnit et plusieurs Childrens OrgUnit, une configuration spécifique est nécessaire pour éviter les erreurs de cascade :
public class OrgUnit
{
public int Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; }
public ICollection<OrgUnit> Childrens { get; set; }
}
// Configuration
public void Configure(EntityTypeBuilder<OrgUnit> builder)
{
builder.ToTable("T_OrgUnits");
builder.Property(o => o.Name).IsUnicode().IsRequired().HasMaxLength(50);
// La relation parent peut être facultative pour le nœud racine
builder.HasOne<OrgUnit>(o => o.Parent).WithMany(o => o.Childrens).IsRequired(false);
}
L'ajout de .IsRequired(false) à la relation Parent est crucial pour permettre aux nœuds racines de ne pas avoir de parent, évitant ainsi les erreurs de cascade lors de la création de la contrainte de clé étrangère.
Différence entre IQueryable et IEnumerable
IQueryable: Les opérations sont traduites en expressions qui sont exécutées sur le serveur (base de données). L'évaluation se fait côté serveur.
IQueryable<Article> serverSideQuery = ctx.Articles.Where(a => a.Title.Contains("Microsoft"));
// La requête SQL est générée et exécutée lors de l'itération ou d'une méthode de terminaison.
foreach (var article in serverSideQuery)
{
Console.WriteLine(article.Title);
}
IEnumerable: Les opérations sont exécutées en mémoire après que les données ont été récupérées du serveur. L'évaluation se fait côté client.
IEnumerable<Article> clientSideQuery = ctx.Articles.AsEnumerable().Where(a => a.Title.Contains("Microsoft"));
// Une requête SQL plus simple est exécutée, puis le filtrage se fait en mémoire.
foreach (var article in clientSideQuery)
{
Console.WriteLine(article.Title);
}
Réutilisation de IQueryable
IQueryable permet de construire des requêtes complexes de manière modulaire. Les conditions sont appliquées uniquement lorsque la requête est exécutée par une méthode de terminaison (par exemple, Count(), ToList(), Max()).
IQueryable<Article> baseQuery = ctx.Articles.Where(b => b.Title.Contains("Microsoft"));
// Ces appels exécutent la requête SQL en incluant la condition de baseQuery
Console.WriteLine(baseQuery.Count());
Console.WriteLine(baseQuery.Max(b => b.Id));
Chaque opération de terminaison sur baseQuery générera une requête SQL distincte qui inclut la clause WHERE définie initialement.
Modes de Lecture des Données avec IQueryable
EF Core utilise principalement le modèle DataReader pour lire les données, ce qui signifie une lecture par lots optimisée pour la mémoire.
DataReader: Lecture séquentielle et progressive des données. Faible utilisation de la mémoire, mais connexion à la base de données potentiellement plus longue.DataTable: Chargement de toutes les données en mémoire en une seule fois. Utilisation mémoire plus élevée, mais connexion libérée plus rapidement.
Pour forcer le chargement de toutes les données en mémoire (équivalent à DataTable), utilisez des méthodes comme ToArray() ou ToListAsync().
// Lecture complète en mémoire
foreach(var article in ctx.Articles.ToArray())
{
Console.WriteLine(article.Title);
Thread.Sleep(10); // Simulation d'un traitement lent
}
Quand charger toutes les données en mémoire ?
- Lorsque le traitement des données récupérées est très long.
- Si vous retournez une
IQueryabled'une méthode qui détruit leDbContextimmédiatement après. - Lors de l'imbrication de plusieurs boucles de lecture de
IQueryable, surtout si le fournisseur de base de données ne supporte pas lesDataReadermultiples simultanés.
Exécution de Requêtes SQL Natives
EF Core permet d'exécuter du SQL brut :
- Exécution de commandes non-query (INSERT, UPDATE, DELETE) :
string nameParam = ";DELETE FROM T_Articles;"; // Exemple d'injection, à éviter en production sans précautions
await ctx.Database.ExecuteSqlInterpolatedAsync($@"INSERT INTO T_Articles (Title, Message)
SELECT Title, {nameParam}
FROM T_Articles WHERE Id >= 3");
- Exécution de requêtes retournant des entités :
string titlePattern = "%Microsoft%";
// Retourne une IQueryable, l'exécution SQL a lieu lors de l'itération
var queryableArticles = ctx.Articles.FromSqlInterpolated($"SELECT * FROM T_Articles WHERE Title LIKE {titlePattern} ORDER BY NEWID()");
foreach (var article in queryableArticles)
{
Console.WriteLine($"{article.Id} - {article.Title}");
}
// Exécution immédiate avec ToList()
var blogs = ctx.Articles.FromSqlRaw("SELECT * FROM T_Articles WHERE Price = '23'").ToList();
Note : Utilisez FromSqlInterpolated pour les requêtes paramétrées et FromSqlRaw pour le SQL brut.
- Exécution de requêtes SQL natives et lecture manuelle :
using (var connection = ctx.Database.GetDbConnection())
{
if (connection.State != System.Data.ConnectionState.Open)
{
await connection.OpenAsync();
}
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT Price, COUNT(*) FROM T_Articles GROUP BY Price";
using (var reader = await command.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
string price = reader.GetString(0);
int count = reader.GetInt32(1);
Console.WriteLine($"{price}: {count}");
}
}
}
}
- Utilisation de Dapper pour les requêtes natives : Dapper peut être intégré pour une exécution de requêtes SQL plus performante et une gestion simplifiée des résultats projetés.
// Définir une classe pour mapper les résultats
public class GroupedArticle
{
public string Price { get; set; }
public int ArticleCount { get; set; } // Renommé pour éviter les conflits potentiels
}
// Utilisation de Dapper
var items = ctx.Database.GetDbConnection().Query<GroupedArticle>("SELECT Price, COUNT(*) AS ArticleCount FROM T_Articles GROUP BY Price");
foreach (var item in items)
{
Console.WriteLine($"{item.Price}: {item.ArticleCount}");
}
Suivi des Changements d'Entités avec DbContext.Entry()
La méthode DbContext.Entry() permet d'obtenir des informations sur l'état d'une entité suivie par le contexte.
États des Entités
Une entité suivie peut se trouver dans l'un des états suivants :
Added: Nouvelle entité ajoutée.Unchanged: Entité non modifiée depuis son chargement.Modified: Entité dont les propriétés ont été modifiées.Deleted: Entité supprimée.Detached: Entité non suivie par le contexte.
Lorsqu'une entité est chargée, EF Core crée une "instantané" de ses valeurs. DbContext.Entry(entity).State reflète l'état actuel. DebugView.LongView offre un aperçu détaillé des changements.
var articles = ctx.Articles.Take(3).ToArray();
var article1 = articles[0];
var article2 = articles[1];
var article3 = articles[2];
var newArticle = new Article { Title = "Test Title", Message = "Test Message" };
// Nouvelle entité non encore ajoutée au contexte
var detachedArticle = new Article { Title = "Detached", Message = "Unfollowed" };
article1.Price += 1; // Marque article1 comme Modified
ctx.Remove(article2); // Marque article2 comme Deleted
ctx.Add(newArticle); // Marque newArticle comme Added
EntityEntry entry1 = ctx.Entry(article1);
EntityEntry entry2 = ctx.Entry(article2);
EntityEntry entry3 = ctx.Entry(article3); // Sera Unchanged
EntityEntry entryNew = ctx.Entry(newArticle);
EntityEntry entryDetached = ctx.Entry(detachedArticle); // Sera Detached
Console.WriteLine($"Article 1 State: {entry1.State}"); // Output: Modified
Console.WriteLine($"Article 2 State: {entry2.State}"); // Output: Deleted
Console.WriteLine($"Article 3 State: {entry3.State}"); // Output: Unchanged
Console.WriteLine($"New Article State: {entryNew.State}"); // Output: Added
Console.WriteLine($"Detached Article State: {entryDetached.State}"); // Output: Detached
Pour améliorer les performances lorsque vous récupérez des données sans intension de les modifier, utilisez AsNoTracking() :
var readOnlyArticles = ctx.Articles.AsNoTracking().Where(a => a.Title.Contains("Info")).ToList();
Filtres de Requête Globaux
Les filtres de requête globaux appliquent automatiquement des clauses WHERE à toutes les requêtes pour un type d'entité donné. Ceci est utile pour implémenter des suppressions logiques (soft delete).
Configuration dans OnModelCreating :
builder.HasQueryFilter(a => a.IsDeleted == false);
Pour ignorer temporairement le filtre global lors d'une requête spécifique :
var articles = ctx.Articles.IgnoreQueryFilters().Take(3).ToArray();
Gestion de la Concurrence
EF Core propose des mécanismes pour gérer les conflits lorsque plusieurs utilisateurs tentent de modifier la même donnée simultanément.
Verrouillage Optimiste (Pessimistic Locking) - Exemple MySQL
Utilise le verrouillage au niveau de la base de données (SELECT ... FOR UPDATE) pour bloquer les enregistrements pendant la transaction.
using (var ctx = new MyDbContext()) // Remplacez MyDbContext par votre contexte
using (var transaction = ctx.Database.BeginTransaction())
{
Console.WriteLine($"{DateTime.Now}: Tentative de verrouillage...");
// Utilise FOR UPDATE pour verrouiller la ligne
var house = ctx.Houses.FromSqlRaw("SELECT * FROM T_House WHERE Id=1 FOR UPDATE").Single();
Console.WriteLine($"{DateTime.Now}: Verrouillage obtenu.");
if (!string.IsNullOrEmpty(house.Owner))
{
Console.WriteLine($"La maison est déjà occupée par {house.Owner}.");
transaction.Rollback(); // Libère le verrou
return;
}
house.Owner = "Nouveau Propriétaire";
Thread.Sleep(5000); // Simule un traitement long
ctx.SaveChanges();
transaction.Commit(); // Déverrouille la ligne
Console.WriteLine("Mise à jour réussie et verrouillage levé.");
}
Verrouillage Optimiste (Optimistic Locking)
Cette approche vérifie les changements à la sauvegarde sans verrouiller la ligne pendant le traitement.
Exemple MySQL : Utilisation de IsConcurrencyToken()
Configure une propriété (souvent un timestamp ou un numéro de version) pour détecter les modifications concurrentes.
public class HouseConfig : IEntityTypeConfiguration<House>
{
public void Configure(EntityTypeBuilder<House> builder)
{
builder.ToTable("T_House");
builder.Property(b => b.Name).IsRequired();
// Marque la propriété 'Owner' comme jeton de concurrence
builder.Property(b => b.Owner).IsConcurrencyToken();
}
}
// Dans votre méthode Main ou service
using (var ctx = new MyDbContext())
{
var house = ctx.Houses.Single(h => h.Id == 1);
house.Owner = "Mon Nom"; // Tentative de modification
Thread.Sleep(5000); // Simule un délai
try
{
ctx.SaveChanges(); // EF Core vérifiera si 'Owner' a été modifié par un autre processus
Console.WriteLine("Mise à jour réussie.");
}
catch (DbUpdateConcurrencyException ex)
{
Console.WriteLine("Erreur de concurrence détectée !");
// Récupérer les valeurs actuelles de la base de données
var entry = ex.Entries.First();
var databaseValues = entry.GetDatabaseValues();
if (databaseValues != null)
{
string currentOwner = databaseValues.GetValue<string>("Owner");
Console.WriteLine($"La maison a été prise par '{currentOwner}'.");
}
// Vous pouvez ici implémenter une logique de résolution (ex: fusionner les changements, recharger les données)
}
}
Exemple SQL Server : Utilisation de IsRowVersion()
SQL Server utilise généralement un type ROWVERSION (ou TIMESTAMP) pour la gestion de la concurrence. EF Core peut le configurer avec IsRowVersion().
// Modèle d'entité avec RowVersion
public class House
{
public long Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public byte[] RowVersion { get; set; } // Colonne pour la concurrence
}
// Configuration
public class HouseConfig : IEntityTypeConfiguration<House>
{
public void Configure(EntityTypeBuilder<House> builder)
{
builder.ToTable("T_House");
builder.Property(b => b.Name).IsRequired();
builder.Property(b => b.RowVersion).IsRowVersion(); // Marque comme RowVersion
}
}
La logique de gestion des exceptions DbUpdateConcurrencyException est la même que pour l'exemple MySQL.