Entitätstypen im Besitz

MIT EF Core können Sie Entitätstypen modellieren, die nur für Navigationseigenschaften anderer Entitätstypen angezeigt werden können. Diese werden als nicht eigenständige Entitätstypenbezeichnet. Die Entität, die einen nicht eigenständiger Entitätstyp enthält, ist der Besitzer.

Nicht eigenständige Entitäten sind im Wesentlichen Teil des Besitzers und können ohne ihn nicht existieren. Sie ähneln konzeptionell den Aggregaten. Das bedeutet, dass die nicht eigenständigen Entitäten per Definition auf der abhängigen Seite der Beziehung mit dem Besitzer stehen.

Konfigurieren von nicht eigenständigen Typen

Bei den meisten Anbietern werden Entitätstypen niemals in den Besitz einer Konvention konfiguriert. Sie müssen die Methode OwnsOne explizit in OnModelCreating verwenden oder den Typ mit OwnedAttribute kommentieren, um den Typ als nicht eigenständig 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 nicht eigenständig.

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 sie als nicht eigenständige Entität zu behandeln, wenn sie aus 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 nicht eigenständige 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 of the database model for entity containing owned reference

Weitere Kontexte finden Sie im vollständigen Beispielprojekt.

Tipp

Der nicht eigenständiger Entitätstyp kann als erforderlich gekennzeichnet werden. Weitere Informationen finden Sie unter Erforderlicher 1:1-Abhängiger.

Implizite Schlüssel

Nicht eigenständige Besitzertypen, die mit OwnsOne konfiguriert durch eine Referenznavigation oder ermittelt werden, verfügen immer über eine 1:1-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 erstellter Primärschlüssel als Schatteneigenschaft für den nicht eigenständigen Typ erstellt wird. Der Wert des Schlüssels einer Instanz des nicht eigenständigen Typs entspricht dem Wert des Schlüssels der Besitzerinstanz.

Sammlungen von nicht eigenständigen Typen

Um eine Sammlung von nicht eigenständigen 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 Ersatz-Primärschlüssels für eine neue Eigenschaft unabhängig vom Fremdschlüssel, 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 Sie ein untergeordnetes Element von einem übergeordneten Element in ein anderes verschieben können, was jedoch in der Regel gegen die Aggregatsemantik verstößt.
  • Verwenden des Fremdschlüssels und einer zusätzlichen Eigenschaft als zusammengesetzter Schlüssel. Der zusätzliche Eigenschaftswert muss jetzt nur für ein bestimmtes übergeordnetes Element eindeutig sein (wenn übergeordnete Element {1} also untergeordnetes Element {1,1} hat, kann übergeordnete Element {2} weiterhin untergeordnetes Element {2,1} haben). Indem der Fremdschlüsselteil Teil des Primärschlüssels wird, wird die Beziehung zwischen dem Besitzer und der nicht eigenständigen 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; }
}

Standardmäßig wird als Primärschlüssel, der für die den referenzierten Typ über die Navigationseigenschaft ShippingCenters verwendet wird, ("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:

Sceenshot of the database model for entity containing owned collection

Zuordnen von nicht eigenständigen Typen mit Tabellenaufteilung

Bei Verwendung relationaler Datenbanken werden standardmäßig referenzeigene Typen der gleichen Tabelle wie der Besitzer zugeordnet. Dies erfordert eine Aufteilung der Tabelle in zwei: Einige Spalten werden verwendet, um die Daten des Besitzers zu speichern, und einige Spalten werden verwendet, um Daten der nicht eigenständigen Entität zu speichern. Dies ist ein gängiges Feature, das als Tabellenaufteilungbezeichnet wird.

Standardmäßig benennt EF Core die Datenbankspalten für die Eigenschaften des nicht eigenständigen Entitätstyps 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 nicht eigenständigen Typen

Ein nicht eigenständiger Entitätstyp kann denselben .NET-Typ wie ein nicht eigenständiger Entitätstyp aufweisen, daher reicht der .NET-Typ möglicherweise nicht aus, um einen nicht eigenständigen Typ zu identifizieren.

In diesen Fällen wird die Eigenschaft, die vom Besitzer auf die nicht eigenständige Entität verweist, zur definierenden Navigation des nicht eigenständigen 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, nicht eigenständige Typen

In diesem Beispiel besitzt OrderDetailsBillingAddress und ShippingAddress, die beide StreetAddress-Typen sind. Dann besitzt der DetailedOrder-Typ OrderDetails.

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 nicht eigenständigen 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 Fähigkeit setzt nicht eigenständige Entitätstypen von komplexen Typen in EF6 ab.

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

Konfigurieren von nicht eigenständigen 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 für den nicht eigenständigen Entitätstyp eine Navigation zu definieren, die nicht Teil der Besitzerbeziehung 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 nicht eigenständigen Typen können wie für eigenständige Navigationseigenschaften weiter konfiguriert werden.

Das obige Modell wird dem folgenden Datenbankschema zugeordnet:

Screenshot of the database model for entity containing nested owned references

Speichern nicht eigenständiger Typen in separaten Tabellen

Im Gegensatz zu komplexen EF6-Typen können nicht eigenständige Typen in einer separaten Tabelle vom Besitzer 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 von DetailedOrder die beiden Adressen einer 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 einem nicht eigenständigen Typen vorhanden sind, da in diesem Fall mehrere Entitätstypen der gleichen Tabelle zugeordnet werden würden.

Abfragen von nicht eigenständigen Typen

Beim Abfragen des Besitzers werden standardmäßig eigene Typen eingeschlossen. Es ist nicht erforderlich, die Methode Include zu verwenden, auch wenn die nicht eigenständigen Typen in einer separaten Tabelle gespeichert sind. Basierend auf dem zuvor beschriebenen Modell holt die folgende Abfrage Order, OrderDetails und die beiden nicht eigenständigenStreetAddresses aus der Datenbank:

var order = context.DetailedOrders.First(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 nicht eigenständigen Entitätstypen, aber einige andere sind Einschränkungen, die wir in zukünftigen Versionen möglicherweise entfernen können:

Optionale Einschränkungen

  • Sie können keine DbSet<T> für nicht eigenständige Typen erstellen.
  • Sie können nicht Entity<T>() mit einem nicht eigenständigen Typen auf ModelBuilder aufrufen.
  • Instanzen von nicht eigenständigen Entitätstypen können nicht von mehreren Besitzern gemeinsam genutzt werden (dies ist ein bekanntes Szenario für Wertobjekte, die nicht mithilfe von unternehmenseigenen 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 auf nicht eigenständige Entitätstypen nicht NULL sein, es sei denn, sie werden explizit einer separaten Tabelle vom Besitzer zugeordnet.
  • In EF Core 3.x werden die Spalten für nicht eigenständige Entitätstypen der gleichen Tabelle zugeordnet, wie der Besitzer, und immer als Nullwerte gekennzeichnet.