Relations entre les entités
Introduction
Après avoir défini les entités et leur correspondance avec les tables de la base de données, il est nécessaire de modéliser les relations qui les associent. Dans EF Core, les relations reposent sur des clés étrangères. Des propriétés de navigation peuvent ensuite être ajoutées aux entités afin de parcourir ces relations naturellement dans le modèle objet de l'application.
Vue d'ensemble des types de relations
Dans une application, les entités ne sont généralement pas isolées. Elles sont liées entre elles par des relations existant entre les données métier.
Entity Framework Core permet de représenter ces relations à l’aide de propriétés de navigation et de clés étrangères. Selon le nombre d’instances pouvant être associées de chaque côté de la relation, plusieurs cardinalités sont possibles :
-
un-à-un (One-to-One)
-
un-à-plusieurs (One-to-Many)
-
plusieurs-à-un (Many-to-One)
-
plusieurs-à-plusieurs (Many-to-Many)
Les relations One-to-Many et Many-to-One représentent la même association, exprimée depuis deux points de vue différents.
Le schéma ci-dessous présente, pour chaque relation, la représentation métier ainsi que la correspondance dans un modèle relationnel (base de données).
Implémenter une relation One-to-Many
Une relation de type One-to-Many, ou « un-à-plusieurs », associe une occurrence d’une entité à plusieurs occurrences d’une autre entité.
Dans notre exemple :
-
Un client peut passer plusieurs commandes
-
Une commande appartient à un seul client
La relation est représentée dans la base de données par une clé étrangère CustomerId, présente dans la table Orders.
Création des tables
La table Customer contient les informations relatives aux clients.
CREATE TABLE dbo.Customer
(
Id bigint IDENTITY(1,1) NOT NULL,
FirstName nvarchar(100) NOT NULL,
LastName nvarchar(100) NOT NULL,
Email nvarchar(255) NOT NULL,
BirthDate date NULL,
IsActive bit NOT NULL,
CreatedDate datetime2(7) NOT NULL,
CONSTRAINT PK_Customer
PRIMARY KEY CLUSTERED (Id)
);
La table Order contient les commandes passées par les clients.
CREATE TABLE dbo.Orders
(
Id bigint IDENTITY(1,1) NOT NULL,
CustomerId bigint NOT NULL,
OrderDate datetime2(7) NOT NULL,
TotalAmount decimal(18,2) NOT NULL,
CreatedDate datetime2(7) NOT NULL,
CONSTRAINT PK_Orders
PRIMARY KEY CLUSTERED (Id),
CONSTRAINT FK_Orders_Customer
FOREIGN KEY (CustomerId)
REFERENCES dbo.Customer(Id)
);
CREATE INDEX IX_Orders_CustomerId
ON dbo.Orders(CustomerId);
Le modèle relationnel peut être représenté de la manière suivante :
Implémentation des entités
La table Customer est représentée par l’entité de même nom :
public class Customer
{
public long Id { get; set; }
public required string FirstName { get; set; }
public required string LastName { get; set; }
public required string Email { get; set; }
public DateOnly? BirthDate { get; set; }
public bool IsActive { get; set; }
public DateTime CreatedDate { get; set; }
// Propriété de navigation
public ICollection<Order> Orders { get; set; } = [];
}
La propriété Orders est une propriété de navigation de type collection. Elle permet d’accéder à l’ensemble des commandes associées à un client. Elle ne correspond à aucune colonne dans la table Customer. Elle est uniquement utilisée par Entity Framework Core pour représenter la relation entre les deux entités dans le modèle objet.
La table Order est aussi représentée par l’entité de même nom :
public class Order
{
public long Id { get; set; }
public DateTime OrderDate { get; set; }
public decimal TotalAmount { get; set; }
public DateTime CreatedDate { get; set; }
// Propriété et relation représentant la clé étrangère
public long CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
}
L’entité Order contient deux propriétés caractérisant la relation :
La propriété CustomerId représente la clé étrangère présente dans la table Order. Elle est de type long et ne peut donc pas contenir la valeur null. La relation est obligatoire du côté de l'entité Order : chaque commande doit être associée à un client.
La propriété Customer est une propriété de navigation. Lorsque la relation a été chargée par EF Core, elle permet d’accéder au client associé à la commande. La relation est donc représentée dans les deux sens :
-
Customer.Orders : permet d’accéder aux commandes d’un client
-
Order.Customer : permet d’accéder au client d’une commande
Configuration avec l’API Fluent
La configuration du mapping peut être placée dans des classes implémentant l’interface IEntityTypeConfiguration<T>, où T est le nom de l'entité. Cette approche permet de séparer la configuration Entity Framework Core des entités.
Mapping de l’entité Customer
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class CustomerConfiguration : IEntityTypeConfiguration<Customer>
{
public void Configure(EntityTypeBuilder<Customer> builder)
{
builder.ToTable("Customer");
builder.HasKey(customer => customer.Id)
.HasName("PK_Customer");
builder.Property(customer => customer.Id)
.ValueGeneratedOnAdd();
builder.Property(customer => customer.FirstName)
.HasMaxLength(100)
.IsRequired();
builder.Property(customer => customer.LastName)
.HasMaxLength(100)
.IsRequired();
builder.Property(customer => customer.Email)
.HasMaxLength(255)
.IsRequired();
builder.Property(customer => customer.BirthDate)
.HasColumnType("date");
builder.Property(customer => customer.IsActive)
.IsRequired();
builder.Property(customer => customer.CreatedDate)
.HasColumnType("datetime2(7)")
.IsRequired();
}
}
Mapping de l’entité Order
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class OrderConfiguration
: IEntityTypeConfiguration<Order>
{
public void Configure(
EntityTypeBuilder<Order> builder)
{
builder.ToTable("Orders");
builder.HasKey(order => order.Id)
.HasName("PK_Orders");
builder.Property(order => order.Id)
.ValueGeneratedOnAdd();
builder.Property(order => order.CustomerId)
.IsRequired();
builder.Property(order => order.OrderDate)
.HasColumnType("datetime2(7)")
.IsRequired();
builder.Property(order => order.TotalAmount)
.HasPrecision(18, 2)
.IsRequired();
builder.Property(order => order.CreatedDate)
.HasColumnType("datetime2(7)")
.IsRequired();
builder.HasIndex(order => order.CustomerId)
.HasDatabaseName("IX_Orders_CustomerId");
// Mapping de la relation entre Customer et Order
builder.HasOne(order => order.Customer)
.WithMany(customer => customer.Orders)
.HasForeignKey(order => order.CustomerId)
.HasConstraintName("FK_Orders_Customer")
.OnDelete(DeleteBehavior.Restrict);
}
}
Grâce aux conventions de nommage d’EF Core, cette relation pourrait être découverte automatiquement à partir des propriétés Customer.Orders, Order.Customer et Order.CustomerId. La configuration avec l’API Fluent permet néanmoins de rendre la relation explicite et de personnaliser certains éléments, tels que le nom de la contrainte de clé étrangère ou le comportement adopté lors de la suppression d’un client.
La relation peut donc être configurée à l’aide de l'instruction suivante :
builder.HasOne(order => order.Customer)
.WithMany(customer => customer.Orders)
.HasForeignKey(order => order.CustomerId)
.HasConstraintName("FK_Orders_Customer")
.OnDelete(DeleteBehavior.Restrict);
La méthode HasOne indique qu’une commande est associée à un seul client :
La méthode WithMany indique qu’un client peut être associé à plusieurs commandes :
La méthode HasForeignKey précise que la propriété CustomerId de l’entité Order constitue la clé étrangère de la relation :
La méthode OnDelete définit le comportement à appliquer lorsqu’un client est supprimé.
Avec le paramètre DeleteBehavior.Restrict, la suppression d’un client possédant encore des commandes est refusée. Cette option évite de supprimer accidentellement l’historique des commandes de clients. Il est aussi possible d'utiliser l'option DeleteBehavior.Cascade, pour que la suppression d'un client entraîne la suppression de toutes ses commandes de manière automatique.
Prise en compte des configurations
Les configurations sont appliquées dans la méthode OnModelCreating du contexte de données.
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(
ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfiguration(new CustomerConfiguration());
modelBuilder.ApplyConfiguration(new OrderConfiguration());
}
}
Lorsque le nombre d’entités devient important, les configurations peuvent être enregistrées automatiquement à partir de l’assembly contenant le contexte :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(
typeof(ApplicationDbContext).Assembly);
}
Entity Framework Core recherche alors toutes les classes qui implémentent IEntityTypeConfiguration<T> et applique automatiquement leur configuration.
