複合型

データベースに保存されたオブジェクトは、次の 3 つのカテゴリに大きく分けることができます。

  • 非構造化であり、1 つの値を保持するオブジェクト。 たとえば、 intGuidstringIPAddressなどです。 これらは(やや緩く) プリミティブ型と呼ばれます。
  • 複数の値を保持するように構成され、オブジェクトの ID がキー値によって定義されているオブジェクト。 たとえば、 BlogPostCustomerなどです。 これらは エンティティ型と呼ばれます。
  • 複数の値を保持するように構造化されているが、そのオブジェクトの ID を定義するキーがないオブジェクト。 たとえば、 AddressCoordinateMoneyなどです。 これらは 値オブジェクトと呼ばれ、EF Core によって 複合型としてマップされます。

複合型は、エンティティ型内に含まれる 1 つの.NET型に複数のプロパティをグループ化します。独自の ID がないため、個別に追跡またはクエリを実行することはできません。 これにより、複合型は 、値オブジェクトをモデル化する自然な方法になります。

Tip

GitHubに関するこの記事では、完全なサンプル プロジェクトを実行してデバッグできます。

Note

複合型は EF Core 8 で導入され、以降のリリースで大幅に拡張されています。 機能は、それらを導入したバージョンで以下に注釈を付けます。

複合型と所有エンティティ型

複合型が存在する前は、 所有エンティティ型 を使用して、キー プロパティのないオブジェクトをモデル化することをお勧めします。 ただし、所有される型はバックグラウンドで エンティティ型 のままです。非表示のキーと ID があるため、参照セマンティクスで動作します。 これにより、複合型が解決するように設計されている多くの摩擦点が発生します。

主な違いは次のとおりです。

特徴 所有されるエンティティの種類 複合型
Identity 非表示のキーと ID を持つ ID なし。値による比較
インスタンス共有 同じインスタンスを 2 回参照することはできません 同じインスタンスを複数のプロパティに割り当てることができます
割り当てセマンティクス 参照セマンティクス 値セマンティクス (プロパティがコピーされます)
.NET 型 参照型のみ 参照 型または 値型
テーブル マッピング 独自のテーブル、テーブル分割、または JSON コンテナーのテーブル (テーブル分割) または JSON
Navigations 他のエンティティへのナビゲーションを含めることができます ナビゲーションを含めることはできません
一括更新 (ExecuteUpdate) サポートしていません サポートされている

たとえば、顧客の請求先住所を配送先住所と同じに割り当てると、同じエンティティ インスタンスを複数回参照できないため、所有エンティティの種類で失敗します。

var customer = await context.Customers.SingleAsync(c => c.Id == someId);
customer.BillingAddress = customer.ShippingAddress;
await context.SaveChangesAsync(); // Throws with owned entity types

複合型には値セマンティクスがあるため、同じ割り当てによってプロパティがコピーされ、期待どおりに動作します。 同様に、LINQ クエリで 2 つの複雑な値を比較すると、その内容が比較されますが、2 つの所有エンティティを比較すると、その ID が比較されます。

このような理由から、一般に、テーブル分割または JSON マッピングを使用して値オブジェクトをモデル化する場合は、複合型の方が適しています。 これらのシナリオで現在所有エンティティ型を使用しているユーザーは、複合型への切り替えを検討することをお勧めします。

簡単な例

いくつかの関連する値を保持しているが、独自の ID を持たない Address 型について考えてみましょう。

public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

Address その後、顧客/注文モデル間で複数の場所で使用できます。

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

顧客の作成と保存は、通常どおりに動作します。

var customer = new Customer
{
    Name = "Willow",
    Address = new Address
    {
        Line1 = "Barking Gate",
        City = "Walpole St Peter",
        Country = "UK",
        PostCode = "PE14 7AV"
    }
};

context.Add(customer);
await context.SaveChangesAsync();

リレーショナル データベースでは、複合型は独自のテーブルを取得しません。 代わりに、そのプロパティは、含むエンティティのテーブルに追加の列としてインラインで保存されます (これは テーブル分割と呼ばれます)。

INSERT INTO [Customers] ([Name], [Address_City], [Address_Country], [Address_Line1], [Address_Line2], [Address_PostCode])
OUTPUT INSERTED.[Id]
VALUES (@p0, @p1, @p2, @p3, @p4, @p5);

複合型には値セマンティクスがあるため、同じ Address インスタンスを複数のプロパティ間で問題なく共有できます。

// The same Address instance can be assigned to multiple complex properties.
customer.Orders.Add(
    new Order
    {
        Contents = "Tesco Tasty Treats",
        BillingAddress = customer.Address,
        ShippingAddress = customer.Address
    });

await context.SaveChangesAsync();

複合型の構成

ほとんどのエンティティ型とは異なり、複合型は 規則によって検出されませんComplexTypeAttributeを使用して型に注釈を付けるか、複合型としてマップする必要があるプロパティごとにOnModelCreatingComplexProperty Fluent API を呼び出すことによって、明示的に構成する必要があります。

[ComplexType]
public record Address
{
    public required string Line1 { get; init; }
    public string? Line2 { get; init; }
    public required string City { get; init; }
    public required string Country { get; init; }
    public required string PostCode { get; init; }
}

複合型プロパティのファセットの構成

入れ子になった Property ビルダーを使用すると、エンティティ型のプロパティと同様に、複合型のスカラー プロパティを構成できます。たとえば、列名や最大長を設定できます。

modelBuilder.Entity<Order>()
    .ComplexProperty(
        o => o.ShippingAddress,
        b =>
        {
            b.Property(a => a.Line1).HasColumnName("ShipsToStreet").HasMaxLength(100);
            b.Property(a => a.City).HasColumnName("ShipsToCity");
        });

EF Core 11 以降では、複合型ビルダーを最初に取得することなく、ラムダでメンバー アクセスをチェーンすることで、複合型内に入れ子になったプロパティを直接構成できます。

// EF Core 11 allows configuring a complex-type property directly by chaining
// member access, without first obtaining the complex-type builder.
modelBuilder.Entity<Customer>()
    .Property(c => c.Address.Line1)
    .HasMaxLength(200);

参照型と値型

複合型には、.NET参照型 (classまたはrecord) または値型 (EF Core 10 で導入されたstructまたはrecord struct) を指定できます。

変更可能性

参照型のインスタンスは複数のプロパティで共有できるため、プロパティの 1 つを変更すると、使用されるすべての場所で値が変更されます。 これは通常、あなたが望むものではありません。 これを回避し、値オブジェクトに自然に適合する良い方法は、複合型を 不変にして、値を変更するには新しいインスタンスを作成する必要があります。 この記事全体で使用される Address 型は変更できない recordです。したがって、アドレスの変更は with 式で行われます。

// Address is an immutable record, so create a new instance to change a value.
customer.Address = customer.Address with { Line1 = "Peacock Lodge" };

await context.SaveChangesAsync();

まったく新しい Address インスタンスが割り当てられている場合でも、EF は個々のプロパティ レベルで変更を追跡するため、実際に値が変更された列のみが更新されます。

UPDATE [Customers] SET [Address_Line1] = @p0
OUTPUT 1
WHERE [Id] = @p1;

不変性は、変更できない class (init 専用または読み取り専用のプロパティ)、 recordreadonly struct、または readonly record structで表すことができます。 値型 (struct) にはコピー セマンティクスがあるため、値を割り当てると常に値がコピーされ、変更可能な場合でも偶発的な共有の問題が回避されますが、一 般的に変更可能な構造体は C# では推奨されないため、不変の形式を使用することをお勧めします。

Tip

複数のエンティティが実際に同じアドレスを観察し、変更されたときに一緒に更新する必要がある場合は、そのアドレスをエンティティ型として独自の ID でモデル化し、複合 を使用するのではなく、ナビゲーションを介して参照します。

入れ子になった複合型

複合型には他の複合型のプロパティを含めることができるため、構造化オブジェクトを任意の深さに構築できます。 たとえば、 Contact 複合型には、 Address と 1 つ以上の PhoneNumber 複合型の両方が含まれる場合があります。

public record Address(string Line1, string? Line2, string City, string Country, string PostCode);

public record PhoneNumber(int CountryCode, long Number);

public record Contact
{
    public required Address Address { get; init; }
    public required PhoneNumber HomePhone { get; init; }
    public required PhoneNumber WorkPhone { get; init; }
}

テーブル分割を使用してマップすると、入れ子になった複合型の列には、プロパティへの完全なパス (たとえば、 Contact_HomePhone_Number) がプレフィックスとして付けられます。

省略可能な複合型

既定では、複合プロパティが 必要です。CLR プロパティには常に値が必要であり、null 非許容列にマップされます。 EF Core 10 以降では、複合プロパティを null 許容として宣言することで省略可能にすることができます。

public class Customer
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A required (non-nullable) complex property.
    public required Address Address { get; set; }

    // An optional (nullable) complex property.
    public Address? SecondaryAddress { get; set; }

    public List<Order> Orders { get; } = new();
}

public class Order
{
    public int Id { get; set; }
    public required string Contents { get; set; }
    public required Address ShippingAddress { get; set; }
    public required Address BillingAddress { get; set; }
    public Customer Customer { get; set; } = null!;
}

省略可能な複合プロパティを null すると、そのすべての列に NULL 値が返されます。

Note

現在、省略可能な複合型では、複合型に少なくとも 1 つの必須プロパティを定義する 必要 があります。 これは、EF では、プロパティがすべてnullされる複合値とnull複合値を区別するために、少なくとも 1 つの null 非許容列が必要であるためです。

複合型に独自の必須プロパティがない場合は、代わりに識別子プロパティを構成できます。 EF Core では複合型の継承はまだサポートされていませんが、識別子は既定で 必要なシャドウ プロパティ として作成され、上記の要件を満たします。

// GeoLocation has no required property, so configure a discriminator. EF creates
// it as a required shadow property, which satisfies the requirement that an
// optional complex type have at least one required property.
modelBuilder.Entity<Place>()
    .ComplexProperty(p => p.Location, b => b.HasDiscriminator());

複合型のコレクション

EF Core 10 以降では、プロパティは複合型の コレクション を保持できます。 リレーショナル データベースでは、複雑なコレクションは、 ToJson を使用して単一の JSON 列にマップする必要があります。別のテーブルにマップすることはできません。

public class Distributor
{
    public int Id { get; set; }
    public required string Name { get; set; }

    // A collection of complex types, mapped to a single JSON column.
    public List<Address> ShippingCenters { get; set; } = new();
}
// Collections of complex types must be mapped to JSON on relational providers.
modelBuilder.Entity<Distributor>()
    .ComplexCollection(d => d.ShippingCenters, b => b.ToJson());

コレクションの各要素は、配列内に JSON オブジェクトとして格納され、コレクション全体が 1 つの列にマップされます。

CREATE TABLE [Distributors] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [ShippingCenters] json NOT NULL,
    CONSTRAINT [PK_Distributors] PRIMARY KEY ([Id])
);

Note

現在、値型 (struct) のコレクションはサポートされていません。複合コレクション要素には参照型 (class または record) を使用します。

JSON への複合型のマッピング

EF Core 10 では、テーブル分割に加えて、(コレクション以外の) 複合プロパティを、 ToJsonを使用して単一の JSON 列にマッピングできます。

modelBuilder.Entity<Customer>(b =>
{
    b.ComplexProperty(c => c.Address, c => c.ToJson());
    b.ComplexProperty(c => c.SecondaryAddress, c => c.ToJson());
});

その後、各複合値は、複数の列に分散するのではなく、1 つの JSON 列にシリアル化されます。

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] nvarchar(max) NOT NULL,
    [Address] json NOT NULL,
    [SecondaryAddress] json NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);

SQL Server 2025 および Azure SQL では、EF は既定でネイティブ json データ型を使用します。他のデータベースおよび古いSQL Server バージョンでは、JSON はテキスト列に格納されます。 必要に応じて、列の種類を HasColumnType でオーバーライドできます。

テーブル分割とは異なり、JSON マッピングでは、マップされた型 のコレクションを使用でき、他のプロパティと同様に、ドキュメント内の個々のプロパティのクエリと更新を行うことができます。 JSON 列内の値は、 ExecuteUpdateAsyncを使用して一括で効率的に更新することもできます。

複合型プロパティのキーとインデックス

EF Core 11 以降では、キーとインデックスは、コレクション以外の複合型内に入れ子になったスカラー プロパティをターゲットにすることができます。 これはラムダで行うことができます。

// EF Core 11 allows keys and indexes to target scalar properties nested
// inside non-collection complex types.
modelBuilder.Entity<Customer>()
    .HasIndex(c => c.Address.PostCode);

.を使用して複雑なプロパティに移動して、同じパスを名前で構成できます。

modelBuilder.Entity<Customer>()
    .HasIndex("Address.PostCode");

リレーショナル プロバイダーの場合、インデックスは JSON 列にマップされた複合型内のパスをターゲットにすることもできます。 複雑なコレクション パスでは、 [] を使用してすべての要素を参照するか、特定の要素の数値インデクサーを参照します。

// Index a scalar inside every element of a JSON-mapped complex collection.
// Requires a provider/database with JSON index support, such as SQL Server 2025.
modelBuilder.Entity<Distributor>()
    .HasIndex("ShippingCenters[].City");

Note

JSON マップの複合コレクションへのインデックス作成には、SQL Server 2025 などの JSON インデックスをサポートするデータベースが必要です。

詳細については、「 キーインデックスと制約」を参照してください。

エンティティ継承を使用した複合型

EF Core 11 以降では、複合型と JSON 列は、 TPT (型ごとのテーブル) または TPC (具体的な種類のテーブル) を使用するエンティティ型で使用できます。 これにより、これらの継承戦略の柔軟性と、複合型のモデリング能力を組み合わせることができます。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Animal>()
        .UseTptMappingStrategy()
        .ComplexProperty(a => a.Details);
}

変更追跡

EF Core は複合型の個々のプロパティへの変更を追跡するため、 SaveChangesを呼び出すと影響を受ける列のみが更新されます。 変更トラッカーを使用して、この追跡状態を検査および操作できます。

EntityEntry.ComplexPropertyを使用して複雑なプロパティに到達し、そのスカラー プロパティをドリルダウンします。

var addressEntry = context.Entry(customer).ComplexProperty(c => c.Address);
Console.WriteLine($"City is currently: {addressEntry.Property(a => a.City).CurrentValue}");
Console.WriteLine($"Address was modified: {addressEntry.Property(a => a.City).IsModified}");

ComplexPropertyEntry API は、エンティティ EntityEntry API を反映します。CurrentValueの読み取りと設定、IsModifiedの確認と設定、さらに入れ子になった複雑なプロパティまたは複雑なコレクションへの移動を行うことができます。 複雑なプロパティは、エンティティの プロパティ値 API (CurrentValues/OriginalValues) を介して公開されます。

複合型のクエリ

複合型メンバーは、エンティティ自体のプロパティと同様に LINQ クエリで使用できます。これらのメンバーをフィルター処理し、それらを投影し、順序を指定できます。

// Filter and project members of a complex property.
var ukCities = await context.Customers
    .Where(c => c.Address.Country == "UK")
    .Select(c => c.Address.City)
    .ToListAsync();

複合型には値セマンティクスがあるため、クエリ内の複合値全体を比較することもできます。EF では、そのすべてのプロパティが比較されます。

var ordersToHomeAddress = await context.Orders
    .Where(o => o.ShippingAddress == o.BillingAddress)
    .ToListAsync();

JSON にマップされた複合型の場合、EF Core 11 によって EF.Functions.JsonPathExistsが追加され、ドキュメントに特定の JSON パスが存在するかどうかを確認します。

var withPostCode = await context.Customers
    .Where(c => EF.Functions.JsonPathExists(c.Address, "$.PostCode"))
    .ToListAsync();

Limitations

複合型は値オブジェクトのモデリング用に設計されており、エンティティ型のすべての機能を意図的にサポートしているわけではありません。 主な制限事項は次のとおりです。

  • 独自の ID または追跡はありません。 複合型はエンティティの一部としてのみ存在できます。複合型の DbSet<T> を持つことはできません。また、個別に追跡またはクエリを実行することもできません。
  • ナビゲーションはありません。 複合型には、エンティティ型へのナビゲーション プロパティを含めることはできません。
  • 別のテーブルはありません。 リレーショナル データベースでは、複合型は常にコンテナーのテーブル (テーブル分割を介して) または JSON 列に格納されます。独自のテーブルには格納されません。
  • コレクションには JSON が必要です。 リレーショナル プロバイダーでは、複雑なコレクションを ToJsonを使用して JSON にマップする必要があります。テーブル分割を使用してマップすることはできません。
  • 値型のコレクションはサポートされていません。 複合コレクション要素は参照型である必要があります。
  • 省略可能な複合型には、必須プロパティが必要です。 省略可能な (null 許容) 複合型は、少なくとも 1 つの必須プロパティを定義する必要があります。

複合型のサポートは、引き続きリリース間で広がっています。最新の追加情報については 、新しい ページを参照してください。