Aller au contenu principal

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();
}
Comparaison avec Doctrine et JPA
// 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);
Le problème N+1

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


Quel attribut DataAnnotations lie une propriété de navigation à sa clé étrangère ?


Quelle méthode LINQ EF Core charge les données d'une propriété de navigation ?


Qu'est-ce que le problème N+1 en EF Core ?


Pour une relation M:N entre Article et Tag, quelle table EF Core crée-t-il automatiquement en base ?


Quelle méthode permet de charger une navigation imbriquée (auteur de l'article ET articles de cet auteur) ?


Dans OnModelCreating, quelle configuration configure la suppression en cascade ?


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


Bonne pratique - Initialiser les collections de navigation

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


Bonne pratique - Convention de nommage des clés étrangères

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


Bonne pratique - Include uniquement ce dont vous avez besoin

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.

📌 Une solution