Relations entre entités
Notions théoriques
Relation 1:N (un à plusieurs)
Dans MonBlog, un Utilisateur peut avoir plusieurs Articles. C'est une relation 1:N (one-to-many).
Pour la configurer avec EF Core :
// Models/Article.cs
public class Article
{
public int Id { get; set; }
public string Titre { get; set; } = string.Empty;
// Clé étrangère (colonne en base de données)
public int AuteurId { get; set; }
// Propriété de navigation (pas de colonne — charge l'objet Utilisateur)
[ForeignKey("AuteurId")]
public Utilisateur? Auteur { get; set; }
}
// Models/Utilisateur.cs
public class Utilisateur
{
public int Id { get; set; }
public string Nom { get; set; } = string.Empty;
// Collection de navigation (liste des articles de cet utilisateur)
public List<Article> Articles { get; set; } = new();
}
// Doctrine (Symfony)
#[ORM\ManyToOne(targetEntity: Utilisateur::class, inversedBy: 'articles')]
#[ORM\JoinColumn(nullable: false)]
private Utilisateur $auteur;
// JPA (Spring Boot)
@ManyToOne
@JoinColumn(name = "auteur_id")
private Utilisateur auteur;
// EF Core (ASP.NET Core)
public int AuteurId { get; set; }
[ForeignKey("AuteurId")]
public Utilisateur? Auteur { get; set; }
Chargement avec Include (Eager Loading)
Par défaut, EF Core ne charge pas les propriétés de navigation automatiquement (chargement paresseux désactivé par défaut). Il faut utiliser Include() explicitement :
// Charger les articles AVEC leur auteur (une seule requête SQL avec JOIN)
var articles = await _context.Articles
.Include(a => a.Auteur)
.ToListAsync();
// SQL : SELECT * FROM articles JOIN utilisateurs ON ...
// Charger un article avec son auteur ET les commentaires de l'auteur
var article = await _context.Articles
.Include(a => a.Auteur)
.ThenInclude(u => u.Articles) // Navigation imbriquée
.FirstOrDefaultAsync(a => a.Id == id);
Sans Include(), si vous accédez à article.Auteur.Nom dans la vue, EF Core exécutera une requête SQL séparée pour chaque article — c'est le problème N+1 (1 requête pour la liste + N requêtes pour les auteurs). Avec Include(), une seule requête SQL avec JOIN est exécutée.
Ce problème est identique en Doctrine (PHP) et JPA (Java).
Relation M:N (plusieurs à plusieurs)
Un Article peut avoir plusieurs Tags, et un Tag peut être sur plusieurs Articles. C'est une relation M:N (many-to-many).
// Models/Tag.cs
public class Tag
{
public int Id { get; set; }
public string Nom { get; set; } = string.Empty;
// Collection de navigation vers les articles
public List<Article> Articles { get; set; } = new();
}
// Dans Article.cs — ajouter :
public List<Tag> Tags { get; set; } = new();
EF Core 5+ gère les relations M:N sans table de jointure explicite en C#. La table de jointure est créée automatiquement en base de données (ArticleTag).
Pour charger les tags d'un article :
var article = await _context.Articles
.Include(a => a.Tags)
.FirstOrDefaultAsync(a => a.Id == id);
Configurer les relations dans OnModelCreating
Pour des configurations avancées (cascade, table de jointure nommée...) :
// Data/MonBlogContext.cs
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// Suppression en cascade : supprimer un auteur supprime ses articles
modelBuilder.Entity<Article>()
.HasOne(a => a.Auteur)
.WithMany(u => u.Articles)
.HasForeignKey(a => a.AuteurId)
.OnDelete(DeleteBehavior.Cascade);
// Nommer explicitement la table de jointure M:N
modelBuilder.Entity<Article>()
.HasMany(a => a.Tags)
.WithMany(t => t.Articles)
.UsingEntity(j => j.ToTable("article_tag"));
}
Exemple pratique
Entités complètes avec relation 1:N Article → Utilisateur et chargement dans le contrôleur :
// Models/Utilisateur.cs
using System.ComponentModel.DataAnnotations;
namespace MonBlog.Models
{
public class Utilisateur
{
[Key]
public int Id { get; set; }
[Required, MaxLength(100)]
[Display(Name = "Nom complet")]
public string Nom { get; set; } = string.Empty;
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
// Collection de navigation : articles de cet utilisateur
public List<Article> Articles { get; set; } = new();
}
}
// Controllers/ArticlesController.cs — avec Include
public async Task<IActionResult> Index()
{
// Charger les articles avec leur auteur en une seule requête
var articles = await _context.Articles
.Include(a => a.Auteur)
.OrderByDescending(a => a.DateCreation)
.ToListAsync();
return View(articles);
}
public async Task<IActionResult> Details(int? id)
{
if (id == null) return NotFound();
var article = await _context.Articles
.Include(a => a.Auteur) // Charger l'auteur
.Include(a => a.Tags) // Charger les tags
.FirstOrDefaultAsync(a => a.Id == id);
return article == null ? NotFound() : View(article);
}
<!-- Dans la vue Details.cshtml -->
@model MonBlog.Models.Article
<h1>@Model.Titre</h1>
@if (Model.Auteur != null)
{
<p>Par <strong>@Model.Auteur.Nom</strong></p>
}
@foreach (var tag in Model.Tags)
{
<span class="badge bg-secondary">@tag.Nom</span>
}
Test de mémorisation/compréhension
TP pour réfléchir et résoudre des problèmes
Vous allez ajouter la relation entre Article et Utilisateur dans MonBlog.
Étape 1 — Créer l'entité Utilisateur
Toujours initialiser les propriétés de collection (= new()) pour éviter les NullReferenceException quand la navigation n'a pas été chargée. Si Utilisateur.Articles n'est pas initialisée et que vous faites utilisateur.Articles.Count sans Include(), une exception est levée.
Étape 2 — Ajouter la clé étrangère et la navigation dans Article
EF Core reconnaît automatiquement AuteurId comme la clé étrangère vers Utilisateur grâce à la convention [NomEntité]Id. Respectez cette convention pour éviter d'avoir à configurer manuellement les relations dans OnModelCreating.
Étape 3 — Utiliser Include pour charger les articles avec leur auteur
N'utilisez pas Include() pour toutes les propriétés de navigation par défaut. Chargez uniquement les données nécessaires à la vue courante. Charger trop de données inutiles ralentit les requêtes et consomme de la mémoire inutilement.