EF Core は、エンティティ型をデータベース内のテーブルにマッピングする場合に、多くの柔軟性を提供します。 これは、EF によって作成されていないデータベースを使用する必要がある場合にさらに便利になります。
以下の手法はテーブルの観点から説明しますが、ビューにマッピングする場合も同じ結果を得ることができます。
テーブル分割
EF Core では、2 つ以上のエンティティを 1 つの行にマップできます。 これは、 テーブル分割 または テーブル共有と呼ばれます。
コンフィギュレーション
テーブル分割を使用するには、エンティティ型を同じテーブルにマップする必要があります。主キーを同じ列にマップし、1 つのエンティティ型の主キーと同じテーブル内のもう 1 つの主キーの間に少なくとも 1 つのリレーションシップを構成します。
テーブル分割の一般的なシナリオは、パフォーマンスやカプセル化を向上させるために、テーブル内の列のサブセットのみを使用することです。
この例では、 Order
は 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; }
}
必要な構成に加えて、Property(o => o.Status).HasColumnName("Status")
と同じ列にDetailedOrder.Status
をマップする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();
});
ヒント
詳細なコンテキストについては、 完全なサンプル プロジェクト を参照してください。
使用方法
テーブル分割を使用したエンティティの保存とクエリは、他のエンティティと同じ方法で実行されます。
using (var context = new TableSplittingContext())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
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"
}
});
await context.SaveChangesAsync();
}
using (var context = new TableSplittingContext())
{
var pendingCount = await context.Orders.CountAsync(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"Current number of pending orders: {pendingCount}");
}
using (var context = new TableSplittingContext())
{
var order = await context.DetailedOrders.FirstAsync(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.ShippingAddress}");
}
オプションの従属エンティティ
依存エンティティによって使用されるすべての列がデータベースに NULL
されている場合、クエリ時にインスタンスは作成されません。 これにより、オプションの依存エンティティをモデル化できます。この場合、プリンシパルのリレーションシップ プロパティは null になります。 これは、依存するプロパティがすべて省略可能で、 null
に設定されている場合にも発生します。これは予期しない可能性があります。
ただし、追加のチェックはクエリのパフォーマンスに影響する可能性があります。 さらに、依存エンティティ型に独自の依存がある場合は、インスタンスを作成する必要があるかどうかを判断することは簡単ではありません。 これらの問題を回避するには、依存エンティティの種類を必須としてマークできます。詳細については、「 必須の 1 対 1 の依存先 」を参照してください。
コンカレンシー トークン
テーブルを共有するエンティティ型にコンカレンシー トークンがある場合は、他のすべてのエンティティ型にも含まれている必要があります。 これは、同じテーブルにマップされているエンティティの 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; }
}
これは、エンティティ型の分割ごとに SplitToTable
を呼び出すことによって EF7 で実現されます。 たとえば、次のコードは、 Customer
エンティティ型を、上記の Customers
、 PhoneNumbers
、および Addresses
テーブルに分割します。
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");
});
.NET