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");
        });