所有されているエンティティ型

EF Core を使用すると、他のエンティティ型のナビゲーション プロパティにのみ表示できるエンティティ型をモデル化できます。 これらは ''所有エンティティ型'' と呼ばれます。 所有エンティティ型を含むエンティティは、その ''所有者'' です。

所有エンティティは、本質的に所有者の一部であり、所有者なしでは存在できず、概念的には集計に似ています。 つまり、所有エンティティは定義上、所有者とのリレーションシップの依存側にあります。

型を所有として構成する

ほとんどのプロバイダーでは、慣例により、エンティティ型が所有として構成されることはありません。型を所有として構成するには、OnModelCreatingOwnsOne メソッドを明示的に使用するか、OwnedAttribute で型に注釈を付ける必要があります。 この点で、Azure Cosmos DB プロバイダーは例外です。 Azure Cosmos DB はドキュメント データベースであるため、プロバイダーはすべての関連するエンティティ型を既定で所有として構成します。

この例では、StreetAddress は ID プロパティのない型です。 特定の注文の配送先住所を指定するために、Order 型のプロパティとして使用されます。

別のエンティティ型から参照された場合は、OwnedAttribute を使用して所有エンティティとして扱うことができます。

[Owned]
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}
public class Order
{
    public int Id { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

また、OnModelCreatingOwnsOne メソッドを使用して、ShippingAddress プロパティが Order エンティティ型の所有エンティティであることを指定したり、必要に応じて追加のファセットを構成したりすることもできます。

modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);

ShippingAddress プロパティが Order 型でプライベートである場合は、OwnsOne メソッドの文字列バージョンを使用できます。

modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");

上記のモデルは、次のデータベース スキーマにマップされています。

Screenshot of the database model for entity containing owned reference

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

ヒント

所有エンティティ型は必須としてマークできます。詳細については、「必須の一対一の依存関係」を参照してください。

暗黙的なキー

OwnsOne で構成されたか、参照ナビゲーションを介して検出された所有型には常に所有者との一対一のリレーションシップがあります。したがって、外部キーの値が一意であるため、独自のキー値は必要ありません。 前の例では、StreetAddress 型でキー プロパティを定義する必要はありません。

EF Core によりこれらのオブジェクトがどのように追跡されるかを理解するために、主キーが所有型のシャドウ プロパティとして作成されていることを把握しておくと便利です。 所有型のインスタンスのキー値は、所有者インスタンスのキー値と同じになります。

所有型のコレクション

所有型のコレクションを構成するには、OnModelCreatingOwnsMany を使用します。

所有型には主キーが必要です。 .NET 型に適したプロパティの候補がない場合は、EF Core で作成を試みることができます。 しかし、所有型がコレクションを介して定義されている場合は、OwnsOne と同じように、所有者への外部キーと所有インスタンスの外部キーの両方として機能するシャドウ プロパティを作成するだけでは十分ではありません。各所有者に複数の所有型インスタンスが存在する可能性があるため、所有者のキーは所有インスタンスごとに一意の ID を指定するのに十分ではありません。

これには、次の 2 つの最も簡単な解決策があります。

  • 所有者を指す外部キーとは別に、新しいプロパティに代理主キーを定義する。 含まれている値はすべての所有者間で一意である必要があります (たとえば、親 {1} が子 {1} を持つ場合、親 {2} は子 {1} を持つことはできません)。したがって、値には固有の意味がありません。 外部キーは主キーの一部ではないため、その値を変更できます。したがって、子をある親から別の親に移動できます。しかし、これは通常、集計セマンティクスに対して行われます。
  • 外部キーと追加のプロパティを複合キーとして使用する。 追加のプロパティ値は現在、指定された親に対してのみ一意である必要があります (したがって、親 {1} が子 {1,1} を持つ場合でも、親 {2} は子 {2,1} を持つことができます)。 外部キーを主キーの一部にすると、所有者と所有エンティティの間のリレーションシップが変更不可となり、集計セマンティクスがより適切に反映されます。 既定では、EF Core によってこれが行われます。

この例では、Distributor クラスを使用します。

public class Distributor
{
    public int Id { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

既定では、ShippingCenters ナビゲーション プロパティを介して参照される所有型に使用される主キーは ("DistributorId", "Id") になります。ここで、"DistributorId" は FK で、"Id" は一意の int 値です。

別の主キーを構成するには、HasKey を呼び出します。

modelBuilder.Entity<Distributor>().OwnsMany(
    p => p.ShippingCenters, a =>
    {
        a.WithOwner().HasForeignKey("OwnerId");
        a.Property<int>("Id");
        a.HasKey("Id");
    });

上記のモデルは、次のデータベース スキーマにマップされています。

Sceenshot of the database model for entity containing owned collection

テーブル分割を使用する所有型のマッピング

リレーショナル データベースを使用する場合、既定では、参照所有型が所有者と同じテーブルにマップされます。 そのためには、テーブルを 2 つに分割する必要があります。つまり、いくつかの列が所有者のデータを格納するために使用され、またいくつかの列が所有エンティティのデータを格納するために使用されます。 これは、テーブル分割と呼ばれる一般的な機能です。

既定では、Navigation_OwnedEntityProperty パターンに従って、EF Core により、所有エンティティ型のプロパティのデータベース列に名前が付けられます。 そのため、'ShippingAddress_Street' および 'ShippingAddress_City' という名前で 'Orders' テーブルに StreetAddress プロパティが表示されます。

HasColumnName メソッドを使用して、これらの列の名前を変更することができます。

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
        sa.Property(p => p.City).HasColumnName("ShipsToCity");
    });

Note

Ignore などの通常のエンティティ型構成メソッドのほとんどは、同じ方法で呼び出すことができます。

複数の所有型間で同じ .NET 型の共有

所有エンティティ型は、別の所有エンティティ型と同じ .NET 型である場合があります。したがって、.NET 型は所有型を特定するのに十分ではない可能性があります。

そのような場合は、所有者から所有エンティティを指すプロパティが、所有エンティティ型の ''定義ナビゲーション'' になります。 EF Core の観点からは、定義ナビゲーションは、.NET 型と共に型の ID の一部になります。

たとえば、次のクラスでは、ShippingAddressBillingAddress の両方が同じ .NET 型 StreetAddress となります。

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

EF Core でこれらのオブジェクトの追跡対象インスタンスがどのように区別されるかを理解するために、定義ナビゲーションが、所有型の .NET 型と所有者のキーの値と共にインスタンスのキーの一部になると考えると便利な場合があります。

入れ子になった所有型

この例では、OrderDetailsBillingAddressShippingAddress を所有しており、どちらも StreetAddress 型です。 また、OrderDetailsDetailedOrder 型に所有されています。

public class DetailedOrder
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
    public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
    Pending,
    Shipped
}

所有型へのナビゲーションごとに、完全に独立した構成で個別のエンティティ型が定義されます。

入れ子になった所有型に加え、所有型では通常のエンティティを参照できます。これは、所有エンティティが依存側にある限り、所有者または別のエンティティにすることができます。 この機能を使用すると、EF6 の複合型とは別に、所有エンティティ型が設定されます。

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

所有型の構成

このモデルを構成するために、fluent 呼び出しで OwnsOne メソッドをチェーンすることができます。

modelBuilder.Entity<DetailedOrder>().OwnsOne(
    p => p.OrderDetails, od =>
    {
        od.WithOwner(d => d.Order);
        od.Navigation(d => d.Order).UsePropertyAccessMode(PropertyAccessMode.Property);
        od.OwnsOne(c => c.BillingAddress);
        od.OwnsOne(c => c.ShippingAddress);
    });

所有者を指すナビゲーション プロパティを定義するために WithOwner 呼び出しが使用されていることに注目してください。 所有権に含まれていない所有者エンティティ型へのナビゲーションを定義するには、引数を指定せずに WithOwner() を呼び出す必要があります。

OrderDetailsStreetAddress の両方で OwnedAttribute を使用して、この結果を得ることもできます。

さらに、Navigation 呼び出しに注目してください。 非所有ナビゲーション プロパティについては、所有型へのナビゲーション プロパティをさらに構成することができます。

上記のモデルは、次のデータベース スキーマにマップされています。

Screenshot of the database model for entity containing nested owned references

別のテーブルへの所有型の格納

また、EF6 複合型とは異なり、所有型は所有者とは別のテーブルに格納することができます。 所有型を所有者と同じテーブルにマップする規則をオーバーライドする場合は、ToTable を呼び出し、別のテーブル名を指定するだけで済みます。 次の例では、OrderDetails とその 2 つのアドレスを DetailedOrder とは別のテーブルにマップします。

modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });

TableAttribute を使用してこれを行うこともできますが、所有型への複数のナビゲーションがある場合は、複数のエンティティ型が同じテーブルにマップされるため、これは失敗することに注意してください。

所有型に対するクエリの実行

所有者に問い合わせるとき、所有されている型が既定で含まれます。 所有型が別のテーブルに格納されている場合でも、Include メソッドを使用する必要はありません。 前述のモデルに基づいて、次のクエリでは、データベースから OrderOrderDetails および所有されている 2 つのStreetAddresses を取得します。

var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");

制限事項

これらの制限事項のいくつかは所有エンティティ型の動作の基礎となるものですが、他のいくつかは将来のリリースで削除できる可能性がある制約です。

設計上の制約

  • 所有型に対して DbSet<T> を作成することはできません。
  • ModelBuilder で所有型を使用して Entity<T>() を呼び出すことはできません。
  • 所有エンティティ型のインスタンスを複数の所有者が共有することはできません (これは、所有エンティティ型を使用して実装できない値オブジェクトの既知のシナリオです)。

現在の欠点

  • 所有エンティティ型に継承階層を含めることはできません

以前のバージョンの欠点

  • EF Core 2.x では、所有エンティティ型への参照ナビゲーションは、所有者とは別のテーブルに明示的にマップされている場合を除き、null にすることはできません。
  • EF Core 3.x では、所有者と同じテーブルにマップされている所有エンティティ型の列は、常に null 許容としてマークされます。