Mappage de table avancé
EF Core offre beaucoup de flexibilité quand il s’agit de mapper des types d’entités à des tables dans une base de données. Cela devient encore plus utile lorsque vous devez utiliser une base de données qui n’a pas été créée par EF.
Les techniques ci-dessous sont décrites en termes de tables, mais le même résultat peut être obtenu lors du mappage aux vues.
Fractionnement de table
EF Core permet de mapper deux entités ou plus à une seule ligne. Il s’agit de fractionnement de table ou de partage de table.
Configuration
Pour utiliser le fractionnement de table, les types d’entités doivent être mappés à la même table, les clés primaires mappées aux mêmes colonnes et au moins une relation configurée entre la clé primaire d’un type d’entité et une autre dans la même table.
Un scénario courant pour le fractionnement de table utilise uniquement un sous-ensemble des colonnes de la table pour des performances ou une encapsulation supérieures.
Dans cet exemple Order
représente un sous-ensemble de DetailedOrder
.
public class Order
{
public int Id { get; set; }
public OrderStatus? Status { get; set; }
public DetailedOrder DetailedOrder { get; set; }
}
public class DetailedOrder
{
public int Id { get; set; }
public OrderStatus? Status { get; set; }
public string BillingAddress { get; set; }
public string ShippingAddress { get; set; }
public byte[] Version { get; set; }
}
En plus de la configuration requise, nous appelons Property(o => o.Status).HasColumnName("Status")
pour mapper DetailedOrder.Status
à la même colonne que Order.Status
.
modelBuilder.Entity<DetailedOrder>(
dob =>
{
dob.ToTable("Orders");
dob.Property(o => o.Status).HasColumnName("Status");
});
modelBuilder.Entity<Order>(
ob =>
{
ob.ToTable("Orders");
ob.Property(o => o.Status).HasColumnName("Status");
ob.HasOne(o => o.DetailedOrder).WithOne()
.HasForeignKey<DetailedOrder>(o => o.Id);
ob.Navigation(o => o.DetailedOrder).IsRequired();
});
Conseil
Pour plus de contexte, consultez l’exemple de projet complet.
Utilisation
L’enregistrement et l’interrogation d’entités à l’aide du fractionnement de table sont effectués de la même façon que d’autres entités :
using (var context = new TableSplittingContext())
{
context.Database.EnsureDeleted();
context.Database.EnsureCreated();
context.Add(
new Order
{
Status = OrderStatus.Pending,
DetailedOrder = new DetailedOrder
{
Status = OrderStatus.Pending,
ShippingAddress = "221 B Baker St, London",
BillingAddress = "11 Wall Street, New York"
}
});
context.SaveChanges();
}
using (var context = new TableSplittingContext())
{
var pendingCount = context.Orders.Count(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"Current number of pending orders: {pendingCount}");
}
using (var context = new TableSplittingContext())
{
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}
Entité dépendante facultative
Si toutes les colonnes utilisées par une entité dépendante sont NULL
dans la base de données, aucune instance ne sera créée lors de l’interrogation. Cela permet de modéliser une entité dépendante facultative, où la propriété de relation sur le principal serait nulle. Notez que cela se produit également si toutes les propriétés de l’entité dépendante sont facultatives et définies sur null
, ce qui peut ne pas être attendu.
Toutefois, la vérification supplémentaire peut avoir un impact sur les performances des interrogations. En outre, si le type d’entité dépendante a lui-même des dépendants, il devient alors compliqué de déterminer si une instance doit être créée. Le type d’entité dépendante peut être marqué comme obligatoire. Pour en savoir plus, consultez Dépendants un-à-un requis.
Jetons d’accès concurrentiel
Si l’un des types d’entités partageant une table a un jeton de concurrence, il doit également être inclus dans tous les autres types d’entités. Cela est nécessaire afin d’éviter la péremption de la valeur du jeton de concurrence quand une seule des entités mappées à la même table est mise à jour.
Pour éviter d’exposer le jeton de concurrence au code consommateur, il est possible que le créer en tant que propriété cachée :
modelBuilder.Entity<Order>()
.Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");
modelBuilder.Entity<DetailedOrder>()
.Property(o => o.Version).IsRowVersion().HasColumnName("Version");
Héritage
Il est recommandé de lire la page dédiée à l’héritage avant de poursuivre la lecture de cette section.
Les types de dépendants utilisant le fractionnement de table peuvent avoir une hiérarchie d’héritage, mais il existe certaines limitations :
- Le type d’entité dépendant ne peut pas utiliser le mappage TPC, car les types dérivés ne peuvent pas être mappés à la même table.
- Le type d’entité dépendant peut utiliser le mappage TPT, mais seul le type d’entité racine peut utiliser le fractionnement de table.
- Si le type d’entité principal utilise TPC, seuls les types d’entités qui n’ont pas de descendants peuvent utiliser le fractionnement de table. Sinon, les colonnes dépendantes doivent être dupliquées sur les tables correspondant aux types dérivés, ce qui complique toutes les interactions.
Fractionnement d'entité
EF Core permet de mapper une entité à des lignes dans deux tables ou plus. Il s’agit de fractionnement d’entité.
Configuration
Par exemple, considérons une base de données avec trois tables contenant des données client :
- Un tableau
Customers
pour les informations clients - Un tableau
PhoneNumbers
pour le numéro de téléphone du client - Un tableau
Addresses
pour l’adresse du client
Voici les définitions de ces tables dans SQL Server :
CREATE TABLE [Customers] (
[Id] int NOT NULL IDENTITY,
[Name] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [PhoneNumbers] (
[CustomerId] int NOT NULL,
[PhoneNumber] nvarchar(max) NULL,
CONSTRAINT [PK_PhoneNumbers] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_PhoneNumbers_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Addresses] (
[CustomerId] int NOT NULL,
[Street] nvarchar(max) NOT NULL,
[City] nvarchar(max) NOT NULL,
[PostCode] nvarchar(max) NULL,
[Country] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Addresses] PRIMARY KEY ([CustomerId]),
CONSTRAINT [FK_Addresses_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id]) ON DELETE CASCADE
);
Chacune de ces tables serait généralement mappée à son propre type d’entité, avec des relations entre les types. Toutefois, si les trois tables sont toujours utilisées ensemble, il peut alors être plus pratique de les mapper toutes à un seul type d’entité. Par exemple :
public class Customer
{
public Customer(string name, string street, string city, string? postCode, string country)
{
Name = name;
Street = street;
City = city;
PostCode = postCode;
Country = country;
}
public int Id { get; set; }
public string Name { get; set; }
public string? PhoneNumber { get; set; }
public string Street { get; set; }
public string City { get; set; }
public string? PostCode { get; set; }
public string Country { get; set; }
}
Ceci est réalisé dans EF7 en appelant SplitToTable
pour chaque division dans le type d’entité. Par exemple, le code suivant divise le type d’entité Customer
en tables Customers
, PhoneNumbers
et Addresses
indiquées ci-dessus :
modelBuilder.Entity<Customer>(
entityBuilder =>
{
entityBuilder
.ToTable("Customers")
.SplitToTable(
"PhoneNumbers",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.PhoneNumber);
})
.SplitToTable(
"Addresses",
tableBuilder =>
{
tableBuilder.Property(customer => customer.Id).HasColumnName("CustomerId");
tableBuilder.Property(customer => customer.Street);
tableBuilder.Property(customer => customer.City);
tableBuilder.Property(customer => customer.PostCode);
tableBuilder.Property(customer => customer.Country);
});
});
Notez également que, si nécessaire, différents noms de colonnes peuvent être spécifiés pour chacune des tables. Pour configurer le nom de colonne de la table principale, consultez configuration de facette spécifique à la table.
Configuration de la clé étrangère de liaison
La clé étrangère qui lie les tables mappées cible les mêmes propriétés sur lesquelles elle est déclarée. Normalement, elle ne serait pas créé dans la base de données, car elle serait redondante. Toutefois, il existe une exception lorsque le type d’entité est mappé à plusieurs tables. Pour modifier ses facettes, vous pouvez utiliser l’API Fluent de configuration de relation :
modelBuilder.Entity<Customer>()
.HasOne<Customer>()
.WithOne()
.HasForeignKey<Customer>(a => a.Id)
.OnDelete(DeleteBehavior.Restrict);
Limites
- Le fractionnement d’entité ne peut pas être utilisé pour les types d’entités dans les hiérarchies.
- Pour toute ligne de la table principale, il doit y avoir une ligne dans chacune des tables fractionnées (les fragments ne sont pas facultatifs).
Configuration de facette spécifique à une table
Certains modèles de mappage entraînent le mappage de la même propriété CLR à une colonne dans chacune de plusieurs tables différentes. EF7 permet à ces colonnes d’avoir des noms différents. Par exemple, considérons une hiérarchie d’héritage simple :
public abstract class Animal
{
public int Id { get; set; }
public string Breed { get; set; } = null!;
}
public class Cat : Animal
{
public string? EducationalLevel { get; set; }
}
public class Dog : Animal
{
public string? FavoriteToy { get; set; }
}
Avec la stratégie de mappage d’héritage TPT, ces types seront mappés sur trois tables. Toutefois, la colonne de clé primaire de chaque table peut avoir un nom différent. Par exemple :
CREATE TABLE [Animals] (
[Id] int NOT NULL IDENTITY,
[Breed] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Animals] PRIMARY KEY ([Id])
);
CREATE TABLE [Cats] (
[CatId] int NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId]),
CONSTRAINT [FK_Cats_Animals_CatId] FOREIGN KEY ([CatId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId]),
CONSTRAINT [FK_Dogs_Animals_DogId] FOREIGN KEY ([DogId]) REFERENCES [Animals] ([Id]) ON DELETE CASCADE
);
EF7 permet de configurer ce mappage à l’aide d’un générateur de tables imbriquées :
modelBuilder.Entity<Animal>().ToTable("Animals");
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
tableBuilder => tableBuilder.Property(cat => cat.Id).HasColumnName("CatId"));
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
tableBuilder => tableBuilder.Property(dog => dog.Id).HasColumnName("DogId"));
Avec le mappage d’héritage TPC, la propriété Breed
peut également être mappée à différents noms de colonnes dans différentes tables. Par exemple, considérons les tables TPC suivantes :
CREATE TABLE [Cats] (
[CatId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[CatBreed] nvarchar(max) NOT NULL,
[EducationalLevel] nvarchar(max) NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([CatId])
);
CREATE TABLE [Dogs] (
[DogId] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[DogBreed] nvarchar(max) NOT NULL,
[FavoriteToy] nvarchar(max) NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([DogId])
);
EF7 prend en charge ce mappage de table :
modelBuilder.Entity<Animal>().UseTpcMappingStrategy();
modelBuilder.Entity<Cat>()
.ToTable(
"Cats",
builder =>
{
builder.Property(cat => cat.Id).HasColumnName("CatId");
builder.Property(cat => cat.Breed).HasColumnName("CatBreed");
});
modelBuilder.Entity<Dog>()
.ToTable(
"Dogs",
builder =>
{
builder.Property(dog => dog.Id).HasColumnName("DogId");
builder.Property(dog => dog.Breed).HasColumnName("DogBreed");
});