高度なテーブル マッピング

EF Core は、エンティティ型をデータベース内のテーブルにマッピングするときに、豊富な柔軟性を提供します。 これは、EF によって作成されていないデータベースを使用する必要があるときにさらに便利になります。

以下の手法はテーブルの観点から説明しますが、ビューにマッピングするときも同じ結果を達成できます。

テーブル分割

EF Core を使用すると、複数のエンティティを 1 つの行にマップできます。 これは、"テーブル分割" または "テーブル共有" と呼ばれます。

構成

テーブル分割を使用するには、エンティティ型を同じテーブルにマップし、主キーを同じ列にマップし、1 つのエンティティ型の主キーと同じテーブルの別のエンティティ型の主キーの間に少なくとも 1 つのリレーションシップを構成する必要があります。

テーブル分割の一般的なシナリオでは、テーブル内の列のサブセットのみを使用して、パフォーマンスまたはカプセル化を促進します。

この例で OrderDetailedOrder のサブセットを表します。

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

必要な構成に加え、Property(o => o.Status).HasColumnName("Status") を呼び出して DetailedOrder.StatusOrder.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();
    });

ヒント

詳細なコンテキストについては、完全なサンプル プロジェクトに関するページを参照してください。

使用法

テーブル分割を使用したエンティティの保存とクエリは、他のエンティティと同じ方法で実行されます。

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

オプションの依存エンティティ

依存エンティティによって使用されるすべての列がデータベース内の NULL にある場合、クエリの実行時にそのインスタンスは作成されません。 これにより、オプションの依存エンティティをモデル化できます。この場合、プリンシパルのリレーションシップ プロパティは null 値になります。 これは、依存するすべてのプロパティがオプションであって null に設定されている場合にも発生しますが、このことは想定されていない可能性があります。

ただし、追加のチェックはクエリのパフォーマンスに影響する可能性があります。 さらに、依存エンティティ型がそれ自体の依存関係を持つ場合、インスタンスを作成する必要があるかどうかを判断することは簡単ではありません。 これらの問題を回避するため、依存エンティティ型に必須のマークを付けることができます。詳細については、必須の一対一の依存関係に関する記事を参照してください。

コンカレンシー トークン

テーブルを共有するエンティティ型のいずれかにコンカレンシー トークンがある場合は、他のすべてのエンティティ型にも含める必要があります。 これは、エンティティの 1 つだけが同じテーブルにマップされている場合に、古いコンカレンシー トークン値が更新されるのを回避するために必要です。

コンシューマー側のコードにコンカレンシー トークンが公開されないようにする場合は、それをシャドウ プロパティとして作成できます。

modelBuilder.Entity<Order>()
    .Property<byte[]>("Version").IsRowVersion().HasColumnName("Version");

modelBuilder.Entity<DetailedOrder>()
    .Property(o => o.Version).IsRowVersion().HasColumnName("Version");

継承

このセクションに進む前に、継承の専用ページをお読みください。

テーブル分割を使用する依存型は継承階層を持つことができますが、次のようにいくつかの制限があります。

  • 依存エンティティ型は TPC マッピングを使用できません。派生型は同じテーブルにマップできないためです。
  • 依存エンティティ型は TPT マッピングを使用できますが、テーブル分割を使用できるのはルート エンティティ型だけです。
  • プリンシパル エンティティ型が TPC を使用する場合、テーブル分割を使用できるのは子孫を持たないエンティティ型だけです。 それ以外の場合は、派生型に対応するテーブルに依存列を複製する必要があるため、すべての相互作用が複雑になります。

エンティティ分割

EF Core では、エンティティを 2 つ以上のテーブルの行にマップできます。 これは、"エンティティ分割" と呼ばれます。

構成

たとえば、顧客データを保持する 3 つのテーブルを持つデータベースを考えてみます。

  • 顧客情報のための Customers テーブル
  • 顧客の電話番号のための PhoneNumbers テーブル
  • 顧客の住所のための Addresses テーブル

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

これらの各テーブルは、通常、型の間のリレーションシップを使って、独自のエンティティ型にマップされます。 ただし、常に 3 つのテーブルすべてが一緒に使われる場合は、それらすべてを 1 つのエンティティ型にマップする方が便利です。 次に例を示します。

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

EF7 では、これはエンティティ型の分割ごとに SplitToTable を呼び出すことによって実現されます。 たとえば、次のコードでは、Customer エンティティ型が、上に示した CustomersPhoneNumbersAddresses の各テーブルに分割されます。

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

必要な場合は、各テーブルに対して異なる列名を指定できることにも注意してください。 メイン テーブルの列名を構成するには、「テーブル固有のファセット構成」を参照してください。

リンク外部キーの構成

マップされたテーブルをリンクする FK は、それが宣言されているのと同じプロパティをターゲットにしています。 通常は、冗長になるため、データベースには作成されません。 ただし、エンティティ型が複数のテーブルにマップされているときには例外があります。 そのファセットを変更するために、リレーションシップ構成 Fluent API を使用できます。

modelBuilder.Entity<Customer>()
    .HasOne<Customer>()
    .WithOne()
    .HasForeignKey<Customer>(a => a.Id)
    .OnDelete(DeleteBehavior.Restrict);

制限事項

  • エンティティ分割は、階層のエンティティ型には使用できません。
  • メイン テーブル内の行の場合、各分割テーブルに行が必要です (フラグメントは省略可能ではありません)。

テーブル固有のファセット構成

一部のマッピング パターンでは、同じ CLR プロパティが複数の異なるテーブルの列にマップされます。 EF7 では、これらの列に異なる名前を付けることができます。 たとえば、簡単な継承階層を考えてみます。

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

TPT 継承マッピング戦略では、これらの型は 3 つのテーブルにマップされます。 ただし、各テーブルの主キー列の名前は異なっていてもかまいません。 次に例を示します。

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 では、入れ子になったテーブル ビルダーを使って、このマッピングを構成できます。

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

TPC 継承マッピングを使うと、Breed プロパティを異なるテーブルの異なる列名にマップすることもできます。 たとえば、次のような TPC テーブルについて考えます。

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 では、次のテーブル マッピングがサポートされています。

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