Typy złożone

Obiekty zapisane w bazie danych można podzielić na trzy szerokie kategorie:

  • Obiekty, które nie mają struktury i przechowują pojedynczą wartość. Na przykład , int, Guid, string, IPAddress. Są to (nieco luźno) nazywane typami pierwotnymi.
  • Obiekty, które mają strukturę przechowywania wielu wartości i gdzie tożsamość obiektu jest definiowana przez wartość klucza. Na przykład , Blog, Post, Customer. Są to typy jednostek.
  • Obiekty, które mają strukturę przechowywania wielu wartości, ale obiekt nie ma klucza definiującego jego tożsamość. Na przykład , Address, Coordinate, Money. Są one nazywane obiektami wartości, a program EF Core mapuje je jako typy złożone.

Typ złożony grupuje kilka właściwości w jeden typ .NET, który jest zawarty w typie jednostki; nie ma własnej tożsamości i nie można go śledzić ani wykonywać zapytań niezależnie. Dzięki temu złożone typy są naturalnym sposobem modelowania obiektów wartości.

Tip

Możesz uruchomić i debugować w pełnym przykładowym projekcie dla tego artykułu na temat GitHub.

Note

Typy złożone zostały wprowadzone w programie EF Core 8 i zostały znacznie rozszerzone w kolejnych wersjach. Funkcje są oznaczone adnotacjami poniżej z wersją, która je wprowadziła.

Typy złożone a typy jednostek będących własnością

Zanim typy złożone istniały, zalecane były typy jednostek należących do nich w celu modelowania obiektów bez kluczowych właściwości. Jednak typy własności są nadal typami jednostek w tle: mają ukryty klucz i tożsamość, a zatem działają z semantykami referencyjnymi. Powoduje to szereg punktów tarcia, które są przeznaczone do rozwiązywania złożonych typów.

Najważniejsze różnice to:

Aspekt Własnościowe typy jednostek Typy złożone
Tożsamość Posiadanie ukrytego klucza i tożsamości Brak tożsamości; porównywane według wartości
Udostępnianie wystąpień Nie można odwołać się do tego samego wystąpienia dwukrotnie To samo wystąpienie można przypisać do wielu właściwości
Semantyka przydziału Semantyka odwołań Semantyka wartości (właściwości są kopiowane)
Typ platformy .NET Tylko typy odwołań Odwołania lub typy wartości
Mapowanie tabeli Własna tabela, dzielenie tabel lub dane JSON Tabela kontenera (dzielenie tabeli) lub dane JSON
Navigations Może zawierać nawigacje do innych jednostek Nie można zawierać nawigacji
Aktualizacja zbiorcza (ExecuteUpdate) Niewspierane Supported

Na przykład przypisanie adresu rozliczeniowego klienta do tego samego, co adres wysyłki kończy się niepowodzeniem z typami jednostek należących do użytkownika, ponieważ nie można odwoływać się do tego samego wystąpienia jednostki więcej niż raz:

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

Ponieważ typy złożone mają semantyka wartości, to samo przypisanie po prostu kopiuje właściwości i działa zgodnie z oczekiwaniami. Podobnie porównanie dwóch złożonych wartości w zapytaniu LINQ porównuje ich zawartość, podczas gdy porównywanie dwóch należących do nich jednostek porównuje ich tożsamości.

Z tych powodów typy złożone są zazwyczaj lepszym wyborem w przypadku modelowania obiektów wartości z podziałem tabel lub mapowaniem JSON. Użytkownicy korzystający obecnie z typów jednostek należących do tych scenariuszy są zachęcani do rozważenia przejścia na złożone typy.

Prosty przykład

Address Rozważ typ, który zawiera kilka powiązanych wartości, ale nie ma własnej tożsamości:

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 Następnie można go użyć w kilku miejscach w modelu klientów/zamówień:

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

Tworzenie i zapisywanie klienta działa jak zwykle:

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

W relacyjnej bazie danych typ złożony nie otrzymuje własnej tabeli. Zamiast tego jego właściwości są zapisywane w tekście jako dodatkowe kolumny w tabeli zawierającej jednostkę (jest to nazywane podziałem tabeli):

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

Ponieważ typy złożone mają semantyka wartości, to to samo Address wystąpienie może być współużytkowane przez wiele właściwości bez żadnych problemów:

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

Konfigurowanie typów złożonych

W przeciwieństwie do większości typów jednostek typy złożone nie są odnajdywane zgodnie z konwencją. Należy je jawnie skonfigurować przez dodawanie adnotacji do typu za ComplexTypeAttributepomocą metody lub przez wywołanie interfejsu ComplexProperty API Fluent dla OnModelCreating każdej właściwości, która powinna być mapowana jako typ złożony:

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

Konfigurowanie aspektów właściwości typu złożonego

Zagnieżdżony Property konstruktor może służyć do konfigurowania właściwości skalarnych typu złożonego, podobnie jak właściwości typu jednostki — na przykład w celu ustawienia nazwy kolumny lub maksymalnej długości:

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

Począwszy od programu EF Core 11, można skonfigurować właściwość zagnieżdżona wewnątrz typu złożonego bezpośrednio, łącząc dostęp do składowych w lambda bez uprzedniego uzyskania konstruktora typu złożonego:

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

Typy odwołań i wartości

Typ złożony może być typem odwołania .NET (a class lub ) lub recordtypem wartości (a struct lub record struct, wprowadzonym w programie EF Core 10).

Możliwość mutowania

Ponieważ wystąpienie typu odwołania może być współużytkowane przez wiele właściwości, zmutowanie jednej z jego właściwości zmienia wartość wszędzie, gdzie jest używana. Zwykle nie jest to to, co chcesz. Dobrym sposobem, aby go uniknąć — i naturalnym dopasowaniem obiektów wartości — jest uczynienie typu złożonego niezmiennym, tak aby zmiana wartości wymagała utworzenia nowego wystąpienia. Address Typ używany w tym artykule jest niezmiennyrecord; zmiana adresu jest zatem wykonywana przy with użyciu wyrażenia:

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

Mimo że przypisano zupełnie nowe Address wystąpienie, program EF nadal śledzi zmiany na poziomie poszczególnych właściwości, więc tylko kolumny, których wartości rzeczywiście się zmieniły, są aktualizowane:

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

Niezmienność może być wyrażona przy użyciu niezmiennych class (właściwości tylko do inicjowania lub tylko do odczytu), wartości record, lub readonly structreadonly record struct. Typy wartości (struct) mają semantyka kopiowania, dlatego ich przypisywanie zawsze kopiuje wartości i pozwala uniknąć przypadkowego problemu z udostępnianiem, nawet w przypadku modyfikowalnego — ale modyfikowalne struktury są zwykle odradzane w języku C#, więc formularz niezmienny jest nadal zalecany.

Tip

Jeśli kilka jednostek naprawdę powinno obserwować ten sam adres i aktualizować razem, gdy się zmieni, należy modelować adres jako typ jednostki z własną tożsamością i odwoływać się do niego za pośrednictwem nawigacji, a nie używać typu złożonego.

Zagnieżdżone typy złożone

Typ złożony może zawierać właściwości innych typów złożonych, co umożliwia tworzenie obiektów strukturalnych na dowolną głębokość. Na przykład Contact typ złożony może zawierać zarówno jeden, jak Address i co najmniej jeden PhoneNumber złożony typ:

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

Podczas mapowania za pomocą dzielenia tabeli kolumny zagnieżdżonego typu złożonego są poprzedzone pełną ścieżką do właściwości (na przykład Contact_HomePhone_Number).

Opcjonalne typy złożone

Domyślnie wymagana jest właściwość złożona: właściwość CLR musi zawsze mieć wartość i mapuje ją na kolumny niepuste. Począwszy od programu EF Core 10, właściwość złożona może być opcjonalna, deklarując ją jako dopuszczającą wartość 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!;
}

Opcjonalna złożona właściwość, która powoduje null wyświetlenie NULL wartości we wszystkich kolumnach.

Note

Opcjonalny typ złożony wymaga obecnie zdefiniowania co najmniej jednej wymaganej właściwości dla typu złożonego. Dzieje się tak dlatego, że program EF potrzebuje co najmniej jednej kolumny, która nie dopuszcza wartości null, aby odróżnić wartość złożoną null od wartości złożonej, której wszystkie właściwości mają wartość null.

Jeśli typ złożony nie ma własnej wymaganej właściwości, możesz zamiast tego skonfigurować właściwość dyskryminującą. Chociaż program EF Core nie obsługuje jeszcze dziedziczenia dla typów złożonych, dyskryminator jest domyślnie tworzony jako wymagana właściwość w tle , która spełnia powyższe wymaganie:

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

Kolekcje typów złożonych

Począwszy od programu EF Core 10, właściwość może przechowywać kolekcję typów złożonych. W relacyjnych bazach danych złożone kolekcje muszą być mapowane na pojedynczą kolumnę JSON przy użyciu ToJson — nie można ich mapować na inną tabelę:

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

Każdy element kolekcji jest przechowywany jako obiekt JSON wewnątrz tablicy, a cała kolekcja jest mapowana na jedną kolumnę:

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

Note

Kolekcje typów wartości (struct) nie są obecnie obsługiwane; użyj typu odwołania (class lub record) dla złożonych elementów kolekcji.

Mapowanie typów złożonych na format JSON

Oprócz dzielenia tabel program EF Core 10 umożliwia mapowanie właściwości złożonej (innej niż kolekcja) na jedną kolumnę JSON z ToJson:

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

Każda złożona wartość jest następnie serializowana w jedną kolumnę JSON, a nie rozłożona na wiele kolumn:

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

W SQL Server 2025 i Azure SQL program EF domyślnie używa natywnego json typu danych; w innych bazach danych i starszych wersjach SQL Server dane JSON są przechowywane w kolumnie tekstowej. W razie potrzeby można zastąpić typ HasColumnType kolumny.

W przeciwieństwie do dzielenia tabel mapowanie JSON zezwala na kolekcje w obrębie typu mapowanego i umożliwia wykonywanie zapytań i aktualizowanie poszczególnych właściwości wewnątrz dokumentu tak samo jak każda inna właściwość. Wartości w kolumnach JSON można również efektywnie aktualizować zbiorczo za pomocą polecenia ExecuteUpdateAsync.

Klucze i indeksy we właściwościach typów złożonych

Począwszy od platformy EF Core 11, klucze i indeksy mogą być obiektami docelowymi właściwości skalarnych zagnieżdżonych wewnątrz typów złożonych niezwiązanych z kolekcją. Można to zrobić za pomocą lambda:

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

Te same ścieżki można skonfigurować według nazwy, używając polecenia . , aby przejść do właściwości złożonej:

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

W przypadku dostawców relacyjnych indeksy mogą również obejmować ścieżki wewnątrz typów złożonych mapowanych na kolumny JSON. Złożone ścieżki kolekcji służą [] do odwoływania się do wszystkich elementów lub indeksatora liczbowego dla określonego elementu:

// 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

Indeksowanie do zamapowanej złożonej kolekcji JSON wymaga bazy danych obsługującej indeksy JSON, takie jak SQL Server 2025.

Aby uzyskać więcej informacji, zobacz Klucze i indeksy i ograniczenia.

Typy złożone z dziedziczeniem jednostek

Począwszy od platformy EF Core 11, typy złożone i kolumny JSON mogą być używane w typach jednostek korzystających z języka TPT (tabela na typ) lub TPC (tabela-typ-beton). Dzięki temu można połączyć elastyczność tych strategii dziedziczenia z możliwościami modelowania typów złożonych.

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

Śledzenie zmian

Program EF Core śledzi zmiany poszczególnych właściwości typu złożonego, więc tylko kolumny, których dotyczy problem, są aktualizowane po wywołaniu metody SaveChanges. Możesz sprawdzić i manipulować tym stanem śledzenia za pomocą monitora zmian.

Użyj EntityEntry.ComplexProperty polecenia , aby uzyskać dostęp do złożonej właściwości, a następnie przejdź do szczegółów jej właściwości skalarnych:

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

Interfejs ComplexPropertyEntry API dubluje interfejs API jednostkiEntityEntry: można odczytywać i ustawiać , sprawdzać i ustawiać CurrentValueIsModified, a następnie przechodzić do dalszych zagnieżdżonych złożonych właściwości lub złożonych kolekcji. Złożone właściwości są również udostępniane za pośrednictwem interfejsów API wartości właściwości jednostki (CurrentValues/OriginalValues).

Wykonywanie zapytań względem typów złożonych

Składowe typu złożonego mogą być używane w zapytaniach LINQ tak samo jak właściwości samej jednostki — można je filtrować, projektować i porządkować według nich:

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

Ponieważ typy złożone mają semantyka wartości, można również porównać całą złożoną wartość w zapytaniu, a program EF porówna wszystkie jej właściwości:

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

W przypadku typów złożonych mapowanych na format JSON program EF Core 11 dodaje EF.Functions.JsonPathExistselement , który sprawdza, czy dana ścieżka JSON istnieje w dokumencie:

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

Limitations

Typy złożone są przeznaczone do modelowania obiektów wartości i celowo nie obsługują każdej możliwości typów jednostek. Główne ograniczenia to:

  • Brak tożsamości ani śledzenia własnych. Typ złożony może istnieć tylko jako część jednostki; Nie można mieć DbSet<T> typu złożonego ani samodzielnie śledzić ani wykonywać względem niego zapytań.
  • Brak nawigacji. Typ złożony nie może zawierać właściwości nawigacji do typów jednostek.
  • Brak oddzielnej tabeli. W relacyjnych bazach danych typ złożony jest zawsze przechowywany w tabeli kontenera (za pośrednictwem dzielenia tabeli) lub w kolumnie JSON — nigdy w własnej tabeli.
  • Kolekcje wymagają formatu JSON. W przypadku dostawców relacyjnych złożone kolekcje muszą być mapowane na format JSON z ToJson; nie można ich mapować za pomocą dzielenia tabeli.
  • Kolekcje typów wartości nie są obsługiwane. Złożone elementy kolekcji muszą być typami referencyjnymi.
  • Opcjonalne typy złożone wymagają wymaganej właściwości. Opcjonalny typ złożony (dopuszczany do wartości null) musi definiować co najmniej jedną wymaganą właściwość.

Obsługa typów złożonych jest nadal rozszerzana w różnych wersjach; zobacz , jakie są nowe strony dla najnowszych dodatków.