Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
EF peut mapper une hiérarchie de type .NET à une base de données. Cela vous permet d’écrire vos entités .NET dans le code comme d’habitude, à l’aide de types de base et dérivés, et d’avoir EF en toute transparence créer le schéma de base de données approprié, émettre des requêtes, etc. Les détails réels de la façon dont une hiérarchie de types est mappée dépendent du fournisseur ; cette page décrit la prise en charge de l’héritage dans le contexte d’une base de données relationnelle.
Mappage de hiérarchie de type d’entité
Par convention, EF n’analyse pas automatiquement les types de base ou dérivés ; Cela signifie que si vous souhaitez qu’un type CLR dans votre hiérarchie soit mappé, vous devez spécifier explicitement ce type sur votre modèle. Par exemple, si vous spécifiez uniquement le type de base d’une hiérarchie, EF Core n’inclut implicitement pas tous ses sous-types.
L’exemple suivant expose un DbSet pour Blog
et sa sous-classe RssBlog
. Si Blog
elle a d’autres sous-classes, elle n’est pas incluse dans le modèle.
internal class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
public DbSet<RssBlog> RssBlogs { get; set; }
}
public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
}
public class RssBlog : Blog
{
public string RssUrl { get; set; }
}
Remarque
Les colonnes de base de données sont automatiquement rendues nullables si nécessaire lors de l’utilisation du mappage TPH. Par exemple, la RssUrl
colonne est nullable, car les instances régulières Blog
n’ont pas cette propriété.
Si vous ne souhaitez pas exposer une DbSet
ou plusieurs entités dans la hiérarchie, vous pouvez également utiliser l’API Fluent pour vous assurer qu’elles sont incluses dans le modèle.
Conseil / Astuce
Si vous ne vous fiez pas aux conventions, vous pouvez spécifier le type de base explicitement à l’aide HasBaseType
de . Vous pouvez également utiliser .HasBaseType((Type)null)
pour supprimer un type d’entité de la hiérarchie.
Configuration par table par hiérarchie et discriminateur
Par défaut, EF mappe l’héritage à l’aide du modèle TPH ( table par hiérarchie ). TPH utilise une table unique pour stocker les données de tous les types de la hiérarchie, et une colonne de discriminateur est utilisée pour identifier le type que représente chaque ligne.
Le modèle ci-dessus est mappé au schéma de base de données suivant (notez la colonne Discriminator
créée implicitement, qui identifie quel type de Blog
est stocké dans chaque ligne).
Vous pouvez configurer le nom et le type de la colonne de discrimination et les valeurs utilisées pour identifier chaque type dans la hiérarchie :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator<string>("blog_type")
.HasValue<Blog>("blog_base")
.HasValue<RssBlog>("blog_rss");
}
Dans les exemples ci-dessus, EF a ajouté implicitement le discriminateur en tant que propriété d’ombre sur l’entité de base de la hiérarchie. Cette propriété peut être configurée comme n’importe quelle autre :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property("blog_type")
.HasMaxLength(200);
}
Enfin, le discriminateur peut également être mappé à une propriété .NET régulière dans votre entité :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator(b => b.BlogType);
modelBuilder.Entity<Blog>()
.Property(e => e.BlogType)
.HasMaxLength(200)
.HasColumnName("blog_type");
modelBuilder.Entity<RssBlog>();
}
Lors de la requête d’entités dérivées qui utilisent le modèle TPH, EF Core ajoute un prédicat sur la colonne discriminatrice dans la requête. Ce filtre garantit que nous ne recevons pas de lignes supplémentaires concernant les types de base ou les types dérivés qui ne sont pas dans le résultat. Ce prédicat de filtre est ignoré pour le type d’entité de base, car l’interrogation de l’entité de base obtient des résultats pour toutes les entités de la hiérarchie. Lors de la matérialisation des résultats d’une requête, si nous rencontrons une valeur de discriminateur qui n’est mappée à aucun type d’entité dans le modèle, nous levons une exception puisque nous ne savons pas comment matérialiser les résultats. Cette erreur se produit uniquement si votre base de données contient des lignes avec des valeurs de discrimination, qui ne sont pas mappées dans le modèle EF. Si vous disposez de ces données, vous pouvez marquer le mappage de discriminateur dans le modèle EF Core comme incomplet pour indiquer que nous devons toujours ajouter un prédicat de filtre pour interroger n’importe quel type dans la hiérarchie.
IsComplete(false)
l'appel à la configuration du discriminateur indique que le mappage est incomplet.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.HasDiscriminator()
.IsComplete(false);
}
Colonnes partagées
Par défaut, lorsque deux types d’entités frères dans la hiérarchie ont une propriété portant le même nom, ils sont mappés à deux colonnes distinctes. Toutefois, si leur type est identique, ils peuvent être mappés à la même colonne de base de données :
public class MyContext : DbContext
{
public DbSet<BlogBase> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.HasColumnName("Url");
modelBuilder.Entity<RssBlog>()
.Property(b => b.Url)
.HasColumnName("Url");
}
}
public abstract class BlogBase
{
public int BlogId { get; set; }
}
public class Blog : BlogBase
{
public string Url { get; set; }
}
public class RssBlog : BlogBase
{
public string Url { get; set; }
}
Remarque
Les fournisseurs de bases de données relationnelles, tels que SQL Server, n’utilisent pas automatiquement le prédicat de discriminateur lors de l’interrogation de colonnes partagées lors de l’utilisation d’un cast. La requête Url = (blog as RssBlog).Url
renverra également la valeur Url
pour les lignes associées Blog
. Pour limiter la requête aux RssBlog
entités, vous devez ajouter manuellement un filtre sur le discriminateur, par exemple Url = blog is RssBlog ? (blog as RssBlog).Url : null
.
Configuration par type de table
Dans le modèle de mappage TPT, tous les types sont mappés à des tables individuelles. Les propriétés qui appartiennent uniquement à un type de base ou à un type dérivé sont stockées dans une table qui correspond à ce type. Les tables qui mappent aux types dérivés stockent également une clé étrangère qui joint la table dérivée à la table de base.
modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");
Conseil / Astuce
Au lieu d’appeler ToTable
sur chaque type d’entité, vous pouvez appeler modelBuilder.Entity<Blog>().UseTptMappingStrategy()
chaque type d’entité racine et les noms de table seront générés par EF.
Conseil / Astuce
Pour configurer différents noms de colonnes pour les colonnes clés primaires de chaque table, consultez la configuration de facette spécifique à la table.
EF crée le schéma de base de données suivant pour le modèle ci-dessus.
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL IDENTITY,
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);
CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId]),
CONSTRAINT [FK_RssBlogs_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([BlogId]) ON DELETE NO ACTION
);
Remarque
Si la contrainte de clé primaire est renommée, le nouveau nom est appliqué à toutes les tables mappées à la hiérarchie, les futures versions EF autorisent le changement de nom de la contrainte uniquement pour une table particulière lorsque le problème 19970 est résolu.
Si vous utilisez la configuration en bloc, vous pouvez récupérer le nom de colonne d’une table spécifique en appelant GetColumnName(IProperty, StoreObjectIdentifier).
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
var tableIdentifier = StoreObjectIdentifier.Create(entityType, StoreObjectType.Table);
Console.WriteLine($"{entityType.DisplayName()}\t\t{tableIdentifier}");
Console.WriteLine(" Property\tColumn");
foreach (var property in entityType.GetProperties())
{
var columnName = property.GetColumnName(tableIdentifier.Value);
Console.WriteLine($" {property.Name,-10}\t{columnName}");
}
Console.WriteLine();
}
Avertissement
Dans de nombreux cas, TPT affiche des performances inférieures par rapport au TPH. Pour plus d’informations, consultez la documentation sur les performances.
Avertissement
Les colonnes d’un type dérivé sont mappées à différentes tables. Par conséquent, les contraintes et les index FK composites qui utilisent les propriétés héritées et déclarées ne peuvent pas être créées dans la base de données.
Tableau par configuration de type concret
Dans le modèle de mappage TPC, tous les types sont mappés à des tables individuelles. Chaque table contient des colonnes pour toutes les propriétés du type d’entité correspondant. Cela résout certains problèmes de performances courants avec la stratégie TPT.
Conseil / Astuce
L’équipe EF a démontré et parlé en profondeur du mappage TPC dans un épisode du standup de la communauté des données .NET. Comme avec tous les épisodes Community Standup, vous pouvez regarder l’épisode TPC maintenant sur YouTube.
modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
.ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
.ToTable("RssBlogs");
Conseil / Astuce
Au lieu d’appeler ToTable
sur chaque type d’entité, il suffit d'appeler modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
sur chaque type d’entité racine pour générer les noms de table par convention.
Conseil / Astuce
Pour configurer différents noms de colonnes pour les colonnes clés primaires de chaque table, consultez la configuration de facette spécifique à la table.
EF crée le schéma de base de données suivant pour le modèle ci-dessus.
CREATE TABLE [Blogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
CONSTRAINT [PK_Blogs] PRIMARY KEY ([BlogId])
);
CREATE TABLE [RssBlogs] (
[BlogId] int NOT NULL DEFAULT (NEXT VALUE FOR [BlogSequence]),
[Url] nvarchar(max) NULL,
[RssUrl] nvarchar(max) NULL,
CONSTRAINT [PK_RssBlogs] PRIMARY KEY ([BlogId])
);
Schéma de la base de données TPC
La stratégie TPC est similaire à la stratégie TPT, sauf qu’une table différente est créée pour chaque type concret de la hiérarchie, mais les tables ne sont pas créées pour les types abstraits . par conséquent, le nom « table-per-concrete-type ». Comme avec TPT, la table elle-même indique le type de l’objet enregistré. Cependant, contrairement au mappage TPT, chaque table contient des colonnes pour chaque propriété du type concret et de ses types de base. Les schémas de base de données TPC sont dénormalisés.
Par exemple, envisagez de mapper cette hiérarchie :
public abstract class Animal
{
protected Animal(string name)
{
Name = name;
}
public int Id { get; set; }
public string Name { get; set; }
public abstract string Species { get; }
public Food? Food { get; set; }
}
public abstract class Pet : Animal
{
protected Pet(string name)
: base(name)
{
}
public string? Vet { get; set; }
public ICollection<Human> Humans { get; } = new List<Human>();
}
public class FarmAnimal : Animal
{
public FarmAnimal(string name, string species)
: base(name)
{
Species = species;
}
public override string Species { get; }
[Precision(18, 2)]
public decimal Value { get; set; }
public override string ToString()
=> $"Farm animal '{Name}' ({Species}/{Id}) worth {Value:C} eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Cat : Pet
{
public Cat(string name, string educationLevel)
: base(name)
{
EducationLevel = educationLevel;
}
public string EducationLevel { get; set; }
public override string Species => "Felis catus";
public override string ToString()
=> $"Cat '{Name}' ({Species}/{Id}) with education '{EducationLevel}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Dog : Pet
{
public Dog(string name, string favoriteToy)
: base(name)
{
FavoriteToy = favoriteToy;
}
public string FavoriteToy { get; set; }
public override string Species => "Canis familiaris";
public override string ToString()
=> $"Dog '{Name}' ({Species}/{Id}) with favorite toy '{FavoriteToy}' eats {Food?.ToString() ?? "<Unknown>"}";
}
public class Human : Animal
{
public Human(string name)
: base(name)
{
}
public override string Species => "Homo sapiens";
public Animal? FavoriteAnimal { get; set; }
public ICollection<Pet> Pets { get; } = new List<Pet>();
public override string ToString()
=> $"Human '{Name}' ({Species}/{Id}) with favorite animal '{FavoriteAnimal?.Name ?? "<Unknown>"}'" +
$" eats {Food?.ToString() ?? "<Unknown>"}";
}
Lors de l'utilisation de SQL Server, les tables créées pour cette hiérarchie sont :
CREATE TABLE [Cats] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[EducationLevel] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Cats] PRIMARY KEY ([Id]));
CREATE TABLE [Dogs] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Vet] nvarchar(max) NULL,
[FavoriteToy] nvarchar(max) NOT NULL,
CONSTRAINT [PK_Dogs] PRIMARY KEY ([Id]));
CREATE TABLE [FarmAnimals] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[Value] decimal(18,2) NOT NULL,
[Species] nvarchar(max) NOT NULL,
CONSTRAINT [PK_FarmAnimals] PRIMARY KEY ([Id]));
CREATE TABLE [Humans] (
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence]),
[Name] nvarchar(max) NOT NULL,
[FoodId] uniqueidentifier NULL,
[FavoriteAnimalId] int NULL,
CONSTRAINT [PK_Humans] PRIMARY KEY ([Id]));
Notez que :
Il n'existe pas de tables pour les types
Animal
ouPet
, car ceux-ci sontabstract
dans le modèle objet. N'oubliez pas que C# n'autorise pas les instances de types abstraits et qu'il n'existe donc aucune situation dans laquelle une instance de type abstrait sera enregistrée dans la base de données.Le mappage des propriétés dans les types de base est répété pour chaque type concret. Par exemple, chaque table a une colonne
Name
, et les chats et les chiens ont une colonneVet
.L'enregistrement de certaines données dans cette base de données entraîne les résultats suivants :
Table Chats
Id | Nom | FoodId | Vétérinaire | Niveau d'éducation |
---|---|---|---|---|
1 | Alice | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Master en Administration des Affaires (MBA) |
2 | Mac | 99ca3e98-b26d-4a0c-d4ae-08da7aca624f | Pengelly | Préscolaire |
8 | Baxter | 5dc5019e-6f72-454b-d4b0-08da7aca624f | Hôpital pour animaux de compagnie Bothell | Bsc |
Table Chiens
Id | Nom | FoodId | Vétérinaire | FavoriteToy |
---|---|---|---|---|
3 | Pain grillé | 011aaf6f-d588-4fad-d4ac-08da7aca624f | Pengelly | M. Squirrel |
Table des animaux de la ferme
Id | Nom | FoodId | Valeur | Species (Espèce) |
---|---|---|---|---|
4 | Clyde | 1d495075-f527-4498-d4af-08da7aca624f | 100,00 | Equus africanus asinus |
Table Humains
Id | Nom | FoodId | FavoriteAnimalId |
---|---|---|---|
5 | Wendy | 5418fd81-7660-432f-d4b1-08da7aca624f | 2 |
6 | Arthur | 59b495d4-0414-46bf-d4ad-08da7aca624f | 1 |
9 | Katie | zéro | 8 |
Notez que contrairement au mappage TPT, toutes les informations d’un objet unique sont contenues dans une table unique. Et, contrairement au mappage TPH, il n'y a aucune combinaison de colonne et de ligne dans aucune table où cela n'est jamais utilisé par le modèle. Nous verrons ci-dessous en quoi ces caractéristiques peuvent être importantes pour les requêtes et le stockage.
Génération de clés
La stratégie de mappage d'héritage choisie a des conséquences sur la façon dont les valeurs de clé primaire sont générées et gérées. Les clés dans TPH sont faciles, car chaque instance d’entité est représentée par une seule ligne d’une seule table. Tout type de génération de valeur clé peut être utilisé et aucune contrainte supplémentaire n'est nécessaire.
Pour la stratégie TPT, il y a toujours une ligne dans le tableau mappée au type de base de la hiérarchie. Tout type de génération de clé peut être utilisé sur cette ligne, et les clés des autres tables sont liées à cette table à l'aide de contraintes de clé étrangère.
Les choses se compliquent un peu pour TPC. Tout d’abord, il est important de comprendre qu’EF Core exige que toutes les entités d’une hiérarchie aient une valeur de clé unique, même si les entités ont des types différents. Par exemple, à l’aide de notre exemple de modèle, un chien ne peut pas avoir la même valeur de clé d’ID qu’un chat. Deuxièmement, contrairement à TPT, il n'existe pas de table commune pouvant servir de lieu unique où les valeurs clés vivent et peuvent être générées. Cela signifie qu’une simple colonne Identity
ne peut pas être utilisée.
Pour les bases de données prenant en charge les séquences, les valeurs clés peuvent être générées en utilisant une seule séquence référencée dans la contrainte par défaut de chaque table. Il s'agit de la stratégie utilisée dans les tableaux TPC présentés ci-dessus, où chaque tableau contient les éléments suivants :
[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])
AnimalSequence
est une séquence de base de données créée par EF Core. Cette stratégie est utilisée par défaut pour les hiérarchies TPC lors de l'utilisation du fournisseur de base de données EF Core pour SQL Server. Les fournisseurs de bases de données pour d’autres bases de données prenant en charge les séquences doivent avoir une valeur par défaut similaire. D'autres stratégies de génération de clés utilisant des séquences, telles que les modèles Hi-Lo, peuvent également être utilisées avec TPC.
Bien que les colonnes d’identité standard ne fonctionnent pas avec TPC, il est possible d’utiliser des colonnes Identity si chaque table est configurée avec une valeur initiale appropriée et incrémente afin que les valeurs générées pour chaque table ne soient jamais en conflit. Par exemple:
modelBuilder.Entity<Cat>().ToTable("Cats", tb => tb.Property(e => e.Id).UseIdentityColumn(1, 4));
modelBuilder.Entity<Dog>().ToTable("Dogs", tb => tb.Property(e => e.Id).UseIdentityColumn(2, 4));
modelBuilder.Entity<FarmAnimal>().ToTable("FarmAnimals", tb => tb.Property(e => e.Id).UseIdentityColumn(3, 4));
modelBuilder.Entity<Human>().ToTable("Humans", tb => tb.Property(e => e.Id).UseIdentityColumn(4, 4));
Importante
L’utilisation de cette stratégie rend plus difficile l’ajout de types dérivés ultérieurement, car il nécessite le nombre total de types dans la hiérarchie à connaître au préalable.
SQLite ne prend pas en charge les séquences ou les graines/incréments d'identité, et par conséquent la génération de valeurs de clé entières n'est pas prise en charge lors de l'utilisation de SQLite avec la stratégie TPC. Toutefois, la génération côté client ou les clés globales uniques , telles que les GUID, sont prises en charge sur n’importe quelle base de données, y compris SQLite.
Contraintes de clés étrangères
La stratégie de mappage TPC crée un schéma SQL dénormalisé. C’est une raison pour laquelle certains puristes de base de données y sont opposés. Par exemple, considérons la colonne de clé étrangère FavoriteAnimalId
. La valeur de cette colonne doit correspondre à la valeur de la clé primaire d'un animal. Cela peut être appliqué dans la base de données avec une simple contrainte FK lors de l'utilisation de TPH ou TPT. Par exemple:
CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])
Mais lors de l’utilisation de TPC, la clé primaire d’un animal donné est stockée dans la table correspondant au type concret de cet animal. Par exemple, la clé primaire d'un chat est stockée dans la colonne Cats.Id
, tandis que la clé primaire d'un chien est stockée dans la colonne Dogs.Id
, et ainsi de suite. Cela signifie qu'une contrainte FK ne peut pas être créée pour cette relation.
En pratique, cela ne pose pas de problème tant que l'application ne tente pas d'insérer des données invalides. Par exemple, si toutes les données sont insérées par EF Core et utilisent des navigations pour lier des entités, il est garanti que la colonne FK contiendra des valeurs PK valides à tout moment.
Résumé et conseils
En résumé, le TPH est généralement correct pour la plupart des applications et est un bon choix par défaut pour un large éventail de scénarios. N’ajoutez donc pas la complexité du TPC si vous n’en avez pas besoin. Plus précisément, si votre code interrogera principalement des entités de nombreux types, comme l'écriture de requêtes sur le type de base, penchez-vous alors vers TPH plutôt que TPC.
Cela dit, TPC est également une bonne stratégie de mappage à utiliser lorsque votre code interroge principalement les entités d’un type feuille unique et vos benchmarks montrent une amélioration par rapport à TPH.
N’utilisez le TPT que si des facteurs externes vous y obligent.