継承

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

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

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

次の例では、Blog とそのサブクラス RssBlog の DbSet が公開されています。 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 列は Null 値を許容しますが、これは通常の Blog インスタンスにはそのプロパティがないためです。

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

ヒント

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

Table-Per-Hierarchy と識別子の構成

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

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

Screenshot of the results of querying the Blog entity hierarchy using table-per-hierarchy pattern

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

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

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 では、兄弟の Blog 行の Url 値も返されます。 クエリを RssBlog エンティティに制限するには、Url = blog is RssBlog ? (blog as RssBlog).Url : null などのように、識別子にフィルターを手動で追加する必要があります。

Table-Per-Type 構成

注意

Table-Per-Type (TPT) 機能は、EF Core 5.0 で導入されました。 Table-Per-Concrete type (TPC) は EF6 でサポートされますが、EF Core ではまだサポートされていません。

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

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

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

警告

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