次の方法で共有


継承

EF では、.NET 型階層をデータベースにマップできます。 これにより、基本型と派生型を使用して、通常どおりコードで .NET エンティティを記述し、EF で適切なデータベース スキーマをシームレスに作成したり、クエリを発行したりできます。型階層のマッピング方法の実際の詳細はプロバイダーに依存します。このページでは、リレーショナル データベースのコンテキストでの継承のサポートについて説明します。

エンティティ型階層マッピング

規則により、EF は基本型または派生型を自動的にスキャンしません。つまり、階層内の CLR 型をマップする場合は、モデルでその型を明示的に指定する必要があります。 たとえば、階層の基本型のみを指定しても、EF Core にすべてのサブタイプが暗黙的に含まれるわけではありません。

次の例では、dbSet for Blog とそのサブクラス RssBlogを公開しています。 Blogに他のサブクラスがある場合、モデルには含まれません。

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

TPH マッピングを使用すると、データベース列は必要に応じて自動的に null 許容になります。 たとえば、通常のRssUrl インスタンスにはそのプロパティがないため、Blog列は null 許容です。

階層内の 1 つ以上のエンティティの DbSet を公開したくない場合は、Fluent API を使用して、それらがモデルに含まれていることを確認することもできます。

ヒント

規則に依存しない場合は、HasBaseTypeを使用して基本型を明示的に指定できます。 .HasBaseType((Type)null)を使用して、階層からエンティティ型を削除することもできます。

階層ごとのテーブルと識別子の構成

既定では、EF は 階層ごとのテーブル (TPH) パターンを使用して継承をマップします。 TPH は 1 つのテーブルを使用して階層内のすべての型のデータを格納し、各行が表す型を識別するために識別子列を使用します。

上記のモデルは、次のデータベース スキーマにマップされます (暗黙的に作成された Discriminator 列に注意してください。これは、各行に格納される Blog の種類を識別します)。

階層ごとのテーブル パターンを使用してブログ エンティティ階層に対してクエリを実行した結果のスクリーンショット

識別子列の名前と型、および階層内の各型を識別するために使用される値を構成できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator<string>("blog_type")
        .HasValue<Blog>("blog_base")
        .HasValue<RssBlog>("blog_rss");
}

上の例では、EF は階層の基本エンティティの シャドウ プロパティ として識別子を暗黙的に追加しました。 このプロパティは、他の方法と同様に構成できます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property("blog_type")
        .HasMaxLength(200);
}

最後に、識別子をエンティティの通常の .NET プロパティにマップすることもできます。

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

TPH パターンを使用する派生エンティティに対してクエリを実行する場合、EF Core はクエリの識別子列に述語を追加します。 このフィルターを使用すると、結果にない基本型または兄弟型に対して追加の行が取得されないようにします。 基本エンティティのクエリを実行すると階層内のすべてのエンティティの結果が取得されるため、このフィルター述語は基本エンティティ型に対してスキップされます。 クエリから結果を具体化するときに、モデル内のどのエンティティ型にもマップされていない識別子の値が検出された場合、結果を具体化する方法がわからないため、例外がスローされます。 このエラーは、EF モデルにマップされていない識別子の値を持つ行がデータベースに含まれている場合にのみ発生します。 このようなデータがある場合は、EF Core モデルの識別子マッピングを不完全としてマークして、階層内の任意の型に対してクエリを実行するためのフィルター述語を常に追加する必要があることを示すことができます。 IsComplete(false) 識別子構成の呼び出しによって、マッピングが不完全であることがマークされます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasDiscriminator()
        .IsComplete(false);
}

共有列

既定では、階層内の 2 つの兄弟エンティティ型に同じ名前のプロパティがある場合、それらは 2 つの個別の列にマップされます。 ただし、型が同じ場合は、同じデータベース列にマップできます。

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

SQL Server などのリレーショナル データベース プロバイダーは、キャストを使用するときに共有列のクエリを実行するときに識別子述語を自動的に使用しません。 クエリ Url = (blog as RssBlog).Urlは、兄弟Url行のBlog値も返します。 エンティティ RssBlog クエリを制限するには、識別子にフィルター ( Url = blog is RssBlog ? (blog as RssBlog).Url : null など) を手動で追加する必要があります。

種類ごとのテーブル構成

TPT マッピング パターンでは、すべての型が個々のテーブルにマップされます。 基本型または派生型にのみ属するプロパティは、その型にマップされるテーブルに格納されます。 派生型にマップされるテーブルには、派生テーブルとベース テーブルを結合する外部キーも格納されます。

modelBuilder.Entity<Blog>().ToTable("Blogs");
modelBuilder.Entity<RssBlog>().ToTable("RssBlogs");

ヒント

各エンティティ型で ToTable を呼び出す代わりに、各ルート エンティティ型で modelBuilder.Entity<Blog>().UseTptMappingStrategy() を呼び出すことができます。テーブル名は EF によって生成されます。

ヒント

各テーブルの主キー列に対して異なる列名を構成するには、 テーブル固有のファセット構成を参照してください。

EF は、上記のモデルに対して次のデータベース スキーマを作成します。

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

主キー制約の名前を変更すると、新しい名前が階層にマップされているすべてのテーブルに適用されます。今後の EF バージョンでは、 問題 19970 が修正された場合にのみ、特定のテーブルの制約の名前を変更できます。

一括構成を使用している場合は、 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();
}

Warnung

多くの場合、TPT は TPH と比較してパフォーマンスが低下します。 詳細については、パフォーマンスに関するドキュメントを参照してください

注意事項

派生型の列は異なるテーブルにマップされるため、継承されたプロパティと宣言されたプロパティの両方を使用する複合 FK 制約とインデックスをデータベースに作成することはできません。

Table-per-concrete-type 構成

TPC マッピング パターンでは、すべての型が個々のテーブルにマップされます。 各テーブルには、対応するエンティティ型のすべてのプロパティの列が含まれています。 これは、TPT 戦略に関する一般的なパフォーマンスの問題に対処します。

ヒント

EF チームは、 .NET Data Community Standup のエピソードで TPC マッピングについて詳しい説明と説明を行いました。 すべてのコミュニティスタンドアップエピソードと同様に、 YouTubeでTPCエピソードを見ることができます。

modelBuilder.Entity<Blog>().UseTpcMappingStrategy()
    .ToTable("Blogs");
modelBuilder.Entity<RssBlog>()
    .ToTable("RssBlogs");

ヒント

各エンティティ型で ToTable を呼び出す代わりに、各ルート エンティティ型で modelBuilder.Entity<Blog>().UseTpcMappingStrategy() を呼び出すだけで、規則によってテーブル名が生成されます。

ヒント

各テーブルの主キー列に対して異なる列名を構成するには、 テーブル固有のファセット構成を参照してください。

EF は、上記のモデルに対して次のデータベース スキーマを作成します。

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])
);

TPC データベース スキーマ

TPC 戦略は TPT 戦略に似ていますが、階層内のすべての具象型に対して異なるテーブルが作成されますが、テーブルは抽象型に対して作成されないため、"table-per-concrete-type" という名前になります。 TPT と同様に、テーブル自体は保存されたオブジェクトの型を示します。 ただし、TPT マッピングとは異なり、各テーブルには具象型とその基本データ型のすべてのプロパティの列が含まれます。 TPC データベース スキーマは非正規化されています。

たとえば、次のような階層のマッピングについて考えます。

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

SQL Server を使うと、この階層に対して作成されるテーブルは次のようになります。

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]));

次のことに注意してください。

  • Animal または Pet 型は、オブジェクト モデルにおいて abstract であるため、これらに対するテーブルはありません。 C# では抽象型のインスタンスが許可されないため、抽象型インスタンスがデータベースに保存されるような状況はないことに注意してください。

  • 基本データ型でのプロパティのマッピングが、具象型ごとに繰り返されます。 たとえば、すべてのテーブルに Name 列があり、Cats と Dogs の両方に Vet 列があります。

  • このデータベースにデータを保存すると、次のようになります。

Cats テーブル

ID (アイディー) 名前 FoodId 獣医 教育レベル
1 アリス 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly MBA
2 マック 99ca3e98-b26d-4a0c-d4ae-08da7aca624f Pengelly プレスクール
8 バクスター 5dc5019e-6f72-454b-d4b0-08da7aca624f ボセルペット病院 学士(理学)

Dogs テーブル

ID (アイディー) 名前 FoodId 獣医 FavoriteToy
3 トースト 011aaf6f-d588-4fad-d4ac-08da7aca624f Pengelly リスさん

FarmAnimals テーブル

ID (アイディー) 名前 FoodId 価値
4 クライド 1d495075-f527-4498-d4af-08da7aca624f 100.00 Equus africanus asinus

Humans テーブル

ID (アイディー) 名前 FoodId お気に入り動物ID
5 ウェンディ 5418fd81-7660-432f-d4b1-08da7aca624f 2
6 アーサー 59b495d4-0414-46bf-d4ad-08da7aca624f 1
9 ケイティ 無効 8

TPT マッピングとは異なり、1 つのオブジェクトのすべての情報が 1 つのテーブルに含まれていることに注意してください。 また、TPH マッピングとは異なり、列と行の組み合わせは、それがモデルで使われていないテーブルには含まれません。 以下では、これらの特性がクエリとストレージにとってどのように重要であるかを説明します。

キーの生成

継承マッピング戦略の選択は、主キー値を生成して管理する方法に影響します。 各エンティティ インスタンスは 1 つのテーブル内の 1 つの行で表されるため、TPH のキーは簡単です。 任意の種類のキー値生成を使用でき、追加の制約は必要ありません。

TPT 戦略の場合は、階層の基本データ型にマップされている行がテーブルに常に存在します。 この行では任意の種類のキー生成を使用でき、他のテーブルのキーは、外部キー制約を使ってこのテーブルにリンクされます。

TPC では、処理が少し複雑になります。 まず、EF Core では、エンティティの型が異なる場合でも、階層内のすべてのエンティティに一意のキー値が必要であることを理解しておくことが重要です。 たとえば、このサンプル モデルを使用すると、Dog は Cat と同じ ID キー値を持つことはできません。 第 2 に、TPT とは異なり、キー値を保持し、生成できる単一の場所となる共通テーブルはありません。 これは、単純な Identity 列を使用できないことを意味します。

シーケンスをサポートするデータベースの場合、各テーブルの既定の制約で参照される 1 つのシーケンスを使って、キー値を生成できます。 これは、上で示した TPC テーブルで使われている戦略であり、各テーブルには次のものが含まれます。

[Id] int NOT NULL DEFAULT (NEXT VALUE FOR [AnimalSequence])

AnimalSequence は、EF Core によって作成されたデータベース シーケンスです。 SQL Server 用の EF Core データベース プロバイダーを使うと、この戦略が TPC 階層に対して既定で使われます。 シーケンスをサポートする他のデータベースのデータベース プロバイダーも、同様の既定値を使う必要があります。 Hi-Lo パターンなど、シーケンスを使う他のキー生成戦略も TPC で使用できます。

標準の ID 列は TPC では機能しませんが、各テーブルが適切なシードで構成され、各テーブルに対して生成された値が競合しないように増分する場合は、ID 列を使用できます。 例えば次が挙げられます。

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

Von Bedeutung

この方法を使用すると、階層内の型の合計数を事前に把握しておく必要があるため、後で派生型を追加することが困難になります。

SQLite はシーケンスまたは ID のシードとインクリメントをサポートしていないため、TPC 戦略で SQLite を使うときは、整数キー値の生成はサポートされません。 ただし、クライアント側の生成またはグローバルに一意のキー (GUID など) は、SQLite を含むすべてのデータベースでサポートされます。

外部キー制約

TPC マッピング戦略では、非正規化された SQL スキーマが作成されます。これは、一部のデータベースの純粋主義者がそれに反対している理由の 1 つです。 たとえば、外部キー列 FavoriteAnimalId について考えます。 この列の値は、何かの動物の主キー値と一致する必要があります。 これは、TPH または TPT を使うときは、単純な FK 制約でデータベースに適用できます。 例えば次が挙げられます。

CONSTRAINT [FK_Animals_Animals_FavoriteAnimalId] FOREIGN KEY ([FavoriteAnimalId]) REFERENCES [Animals] ([Id])

ただし、TPC を使用する場合、特定の動物の主キーは、その動物の具体的な種類に対応するテーブルに格納されます。 たとえば、猫の主キーは Cats.Id 列に格納され、犬の主キーは Dogs.Id 列に格納されます。 これは、このリレーションシップに対しては FK 制約を作成できないことを意味します。

実際には、アプリケーションで無効なデータの挿入を試みない限り、これは問題ではありません。 たとえば、すべてのデータが EF Core によって挿入され、ナビゲーションを使用してエンティティを関連付ける場合、FK 列に有効な PK 値が常に含まれていることが保証されます。

まとめとガイダンス

要約すると、TPH は通常、ほとんどのアプリケーションで問題なく、さまざまなシナリオに適した既定値であるため、TPC が不要な場合は複雑さを追加しないでください。 具体的には、多くの型のエンティティのクエリを実行することが多いコードの場合は (基本データ型に対するクエリの記述など)、TPC より TPH が適しています。

つまり、TPCは、コードが主に単一のリーフ型のエンティティを照会し、ベンチマークがTPHと比較して改善を示す場合に使用するのに適したマッピング戦略でもあります。

TPT は、外部要因によって TPT を使うしかない場合にのみ使ってください。