Freigeben über


Eigene Entitätstypen

Mit EF Core lassen sich Entitätstypen modellieren, die nur in Navigationseigenschaften anderer Entitätstypen auftreten können. Diese werden als besitzende Entitätstypen bezeichnet. Die Entität, die einen nicht-eigenständigen Entitätstyp enthält, ist der Besitzer.

Besitz-Entitäten sind im Wesentlichen Teil des Eigentümers und können ohne ihn nicht existieren. Sie sind konzeptionell ähnlich wie Aggregate. Das bedeutet, dass die besitzergemeinschaftlichen Entitäten per Definition auf der abhängigen Seite der Beziehung mit dem Eigentümer stehen.

Konfigurieren von eigenen Typen

Bei den meisten Anbietern werden Entitätstypen nie durch Konvention als im Besitz konfiguriert. Sie müssen die Methode OwnsOne explizit in OnModelCreating verwenden oder den Typ mit OwnedAttribute annotieren, um den Typ als im Besitz zu konfigurieren. Der Azure Cosmos DB-Anbieter ist eine Ausnahme hiervon. Da Azure Cosmos DB eine Dokumentdatenbank ist, konfiguriert der Anbieter standardmäßig alle zugehörigen Entitätstypen als zugehörig.

In diesem Beispiel ist StreetAddress ein Typ ohne Identitätseigenschaft. Dieser Typ wird als Eigenschaft des Typs „Order“ verwendet, um die Lieferadresse für eine bestimmte Bestellung anzugeben.

Wir können OwnedAttribute verwenden, um es als eine zugeordnete Entität zu behandeln, wenn es von einem anderen Entitätstyp referenziert wird:

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

Es ist auch möglich, die OwnsOne-Methode in OnModelCreating zu verwenden, um anzugeben, dass die ShippingAddress-Eigenschaft eine "zugeordnete Entität" des Entitätstyps Order ist und bei Bedarf zusätzliche Facetten zu konfigurieren.

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

Wenn die Eigenschaft ShippingAddress im Typ Order privat ist, können Sie die Zeichenfolgenversion der Methode OwnsOne verwenden:

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

Das obige Modell wird dem folgenden Datenbankschema zugeordnet:

Screenshot des Datenbankmodells für Entität mit besitztem Verweis

Weitere Kontexte finden Sie im vollständigen Beispielprojekt.

Tipp

Der zugeordneter Entitätstyp kann als erforderlich markiert werden. Weitere Informationen finden Sie unter Erforderliche Eins-zu-eins-Abhängigkeiten.

Implizite Schlüssel

Besitztypen, die mit OwnsOne konfiguriert oder durch eine Referenznavigation erkannt werden, haben immer eine Eins-zu-eins-Beziehung mit dem Besitzer. Daher benötigen sie keine eigenen Schlüsselwerte, da die Fremdschlüsselwerte eindeutig sind. Im vorherigen Beispiel muss der Typ StreetAddress keine Schlüsseleigenschaft definieren.

Um zu verstehen, wie EF Core diese Objekte nachverfolgt, ist es hilfreich zu wissen, dass ein Primärschlüssel als Schatteneigenschaft für den besitzenden Typ erstellt wird. Der Wert des Schlüssels einer Instanz des Eigentypen wird der gleiche sein wie der Wert des Schlüssels der Besitzerinstanz.

Sammlungen von eigenen Typen

Um eine Sammlung von eigenen Typen zu konfigurieren, verwenden Sie OwnsMany in OnModelCreating.

Besitzertypen benötigen einen Primärschlüssel. Wenn es keine guten Kandidateneigenschaften für den .NET-Typ gibt, kann EF Core versuchen, einen zu erstellen. Wenn nicht eigenständige Typen jedoch über eine Sammlung definiert werden, reicht es nicht aus, nur eine Schatteneigenschaft zu erstellen, um sowohl als Fremdschlüssel als auch als Primärschlüssel der nicht eigenständigen Instanz zu fungieren, wie wir es für OwnsOne tun: Es können mehrere nicht eigenständige Typinstanzen für jeden Besitzer vorhanden sein, und daher reicht der Schlüssel des Besitzers nicht aus, um eine eindeutige Identität für jede eigene Instanz bereitzustellen.

Die beiden einfachsten Lösungen hierfür sind:

  • Definieren eines Ersatzprimärschlüssels für eine neue Eigenschaft, die unabhängig vom Fremdschlüssel ist, der auf den Besitzer verweist. Die enthaltenen Werte müssen für alle Besitzer eindeutig sein (z. B. wenn das übergeordnete Element {1} das untergeordnete Element {1} hat, dann kann das übergeordnete Element {2} nicht das untergeordnetes Element {1} haben), sodass der Wert keine inhärente Bedeutung hat. Da der Fremdschlüssel nicht Teil des Primärschlüssels ist, können seine Werte geändert werden, sodass ein Kind von einem Elternteil zu einem anderen verschoben werden darf, was jedoch normalerweise gegen die Regeln der Aggregatsemantik verstößt.
  • Verwenden des Fremdschlüssels und einer weiteren Eigenschaft als zusammengesetzter Schlüssel. Der zusätzliche Eigenschaftswert muss jetzt nur für ein bestimmtes übergeordnetes Element eindeutig sein (wenn zum Beispiel übergeordnetes Element {1} ein untergeordnetes Element {1,1} hat, kann übergeordnetes Element {2} weiterhin ein untergeordnetes Element {2,1} haben). Indem der Fremdschlüssel Teil des Primärschlüssels wird, wird die Beziehung zwischen dem Besitzer und der besitzten Entität unveränderlich und spiegelt die aggregierte Semantik besser wider. Dies führt EF Core standardmäßig durch.

In diesem Beispiel verwenden wir die Klasse Distributor.

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

Der standardmäßig für den über die Navigationseigenschaft ShippingCenters referenzierten Typ verwendete Primärschlüssel ist ("DistributorId", "Id"), wobei "DistributorId" der FK ist und "Id" ein eindeutiger int-Wert ist.

Um einen anderen Primärschlüsselaufruf HasKey zu konfigurieren.

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

Das obige Modell wird dem folgenden Datenbankschema zugeordnet:

Screenshot des Datenbankmodells für eine Entität mit besitzergreifender Sammlung

Zuordnen von eigenen Typen mit Tabellensplitting

Bei Verwendung relationaler Datenbanken werden in der Standardkonfiguration referenzierte Typen der gleichen Tabelle wie der Besitzer zugeordnet. Dies erfordert eine Aufteilung der Tabelle in zwei Teile: Einige Spalten werden verwendet, um die Daten des Besitzers zu speichern, und andere Spalten werden für die Daten der zugehörigen Entität genutzt. Dies ist ein gängiges Feature, das als Tabellenaufteilungbezeichnet wird.

Standardmäßig benennt EF Core die Datenbankspalten für die Eigenschaften des Besitzertyps der Entität nach dem Muster Navigation_OwnedEntityProperty. Daher werden die StreetAddress-Eigenschaften in der Tabelle ‚Bestellungen‘ mit den Namen ‚ShippingAddress_Street‘ und ‚ShippingAddress_City‘ angezeigt.

Sie können die HasColumnName-Methode verwenden, um diese Spalten umzubenennen.

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

Hinweis

Die meisten der normalen Konfigurationsmethoden des Entitätstyps wie Ignore können auf die gleiche Weise aufgerufen werden.

Teilen desselben .NET-Typs unter mehreren besessenen Typen

Ein Eigentumstyp kann denselben .NET-Typ wie ein anderer Eigentumstyp aufweisen, daher reicht der .NET-Typ möglicherweise nicht aus, um einen Eigentumstyp eindeutig zu identifizieren.

In diesen Fällen wird die Eigenschaft, die vom Besitzer auf die abhängige Entität verweist, zur definierenden Navigation des abhängigen Entitätstyps. Aus Sicht von EF Core ist die definierende Navigation Teil der Identität des Typs zusammen mit dem .NET-Typ.

Zum Beispiel gehören in der folgenden Klasse ShippingAddress und BillingAddress beide dem gleichen .NET-Typ StreetAddress an.

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

Um zu verstehen, wie EF Core nachverfolgte Instanzen dieser Objekte unterscheidet, kann es hilfreich sein, zu denken, dass die definierende Navigation Teil des Schlüssels der Instanz neben dem Wert des Schlüssels des Besitzers und des .NET-Typs des besitzereigenen Typs geworden ist.

Geschachtelte Eigentumstypen

In diesem Beispiel besitzt OrderDetails sowohl BillingAddress als auch ShippingAddress, die beide StreetAddress-Typen sind. Dann wird OrderDetails vom DetailedOrder-Typ besessen.

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

Jede Navigation zu einem eigenen Typ definiert einen separaten Entitätstyp mit vollständig unabhängiger Konfiguration.

Zusätzlich zu geschachtelten Besitzertypen kann ein nicht eigenständiger Typ auf eine normale Entität verweisen, die entweder der Besitzer oder eine andere Entität sein kann, solange die nicht eigenständige Entität auf der abhängigen Seite ist. Diese Eigenschaft unterscheidet in EF6 definierte Entitätstypen von komplexen Typen.

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

Konfigurieren von besessenen Typen

Es ist möglich, die OwnsOne-Methode in einen Fluent-Aufruf zu verketten, um dieses Modell zu konfigurieren:

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

Beachten Sie, dass der WithOwner-Aufruf verwendet wird, um die Navigationseigenschaft zu definieren, die zurück auf den Besitzer zeigt. Um eine Navigation für den Entitätstyp des Eigentümers zu definieren, der nicht Teil der Ownership-Beziehung ist, sollte WithOwner() ohne Argumente aufgerufen werden.

Es ist auch möglich, dieses Ergebnis mit OwnedAttribute sowohl auf OrderDetails als auch auf StreetAddress zu erzielen.

Beachten Sie außerdem den Aufruf Navigation. Navigationseigenschaften zu eigenständigen Typen können wie für nicht-eigenständige Navigationseigenschaften weiter konfiguriert werden.

Das obige Modell wird dem folgenden Datenbankschema zugeordnet:

Screenshot: Datenbankmodell für Entität mit geschachtelten, zugehörigen Referenzen

Speichern von Besitztypen in separaten Tabellen

Im Gegensatz zu komplexen EF6-Typen können besitzende Typen in einer separaten Tabelle vom Eigentümer gespeichert werden. Um die Konvention außer Kraft zu setzen, die einen eigenen Typ der gleichen Tabelle wie dem Besitzer zuordnet, können Sie einfach ToTable aufrufen und einen anderen Tabellennamen angeben. Im folgenden Beispiel werden OrderDetails und dessen zwei Adressen einer von DetailedOrder separaten Tabelle zugeordnet.

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

Es ist auch möglich, TableAttribute zu verwenden, um das zu erreichen, aber beachten Sie, dass es fehlschlägt, wenn mehrere Navigationen zu den besitzenden Typen vorhanden sind, da in diesem Fall mehrere Entitätstypen der gleichen Tabelle zugeordnet werden würden.

Abfragen von eigenen Typen

Beim Abfragen des Besitzers werden die besessenen Typen standardmäßig eingeschlossen. Es ist nicht erforderlich, die Methode Include zu verwenden, selbst wenn die zugehörigen Typen in einer separaten Tabelle gespeichert sind. Basierend auf dem zuvor beschriebenen Modell holt die folgende Abfrage Order, OrderDetails und die beiden zugehörigen StreetAddresses aus der Datenbank.

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

Begrenzungen

Einige dieser Einschränkungen sind grundlegend für die Funktionsweise von eigenständigen Entitätstypen, aber andere sind Einschränkungen, die wir in zukünftigen Versionen möglicherweise aufheben können.

Designbedingte Einschränkungen

  • Sie können keine DbSet<T> für eigene Typen erstellen.
  • Man kann Entity<T>() nicht mit einem besessenen Typ auf ModelBuilder aufrufen.
  • Instanzen von besessenen Entitätstypen können nicht von mehreren Besitzern geteilt werden (dies ist ein bekanntes Szenario für Wertobjekte, die nicht mithilfe von besessenen Entitätstypen implementiert werden können).

Aktuelle Mängel

  • Besitzer-Entitätstypen können keine Vererbungshierarchien haben

Mängel in früheren Versionen

  • In EF Core 2.x können Referenznavigationen zu besessenen Entitätstypen nicht NULL sein, es sei denn, sie sind explizit einer separaten Tabelle vom Eigentümer zugeordnet.
  • In EF Core 3.x werden die Spalten für gehörende Entitätstypen derselben Tabelle zugeordnet wie der Besitzer und immer als nullable gekennzeichnet.