Dela via


Ägda entitetstyper

MED EF Core kan du modellera entitetstyper som bara kan visas på navigeringsegenskaper för andra entitetstyper. Dessa kallas för ägda entitetstyper. Entiteten som innehåller en ägd entitetstyp är dess ägare.

Ägda entiteter är i princip en del av ägaren och kan inte finnas utan den, de liknar aggregeringar konceptuellt. Det innebär att den ägda entiteten per definition är beroende av relationen med ägaren.

Konfigurera typer som ägda

I de flesta leverantörer konfigureras aldrig entitetens typ som ägd genom konvention – du måste använda OwnsOne-metoden uttryckligen i OnModelCreating eller annotera typen med OwnedAttribute för att konfigurera typen som ägd. Azure Cosmos DB-providern är ett undantag till detta. Eftersom Azure Cosmos DB är en dokumentdatabas konfigurerar providern alla relaterade entitetstyper som ägs som standard.

I det här exemplet StreetAddress är en typ utan identitetsegenskap. Den används som en egenskap av typen Order för att ange leveransadressen för en viss beställning.

Vi kan använda OwnedAttribute för att behandla den som en ägd entitet när den refereras från en annan entitetstyp:

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

Du kan också använda OwnsOne metoden i OnModelCreating för att ange att ShippingAddress egenskapen är en ägd entitet av Order entitetstypen och för att konfigurera ytterligare fasetter om det behövs.

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

Om egenskapen ShippingAddress är privat i Order typen kan du använda strängversionen av OwnsOne metoden:

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

Modellen ovan mappas till följande databasschema:

Skärmbild av databasmodellen för entitet som innehåller en ägd referens

Mer kontext finns i fullständiga exempelprojektet.

Tips/Råd

Den ägda entitetstypen kan markeras som obligatorisk. Mer information finns i Obligatoriska en-till-en-beroenden.

Implicita nycklar

Ägda typer som konfigureras med OwnsOne eller identifieras via en referensnavigering har alltid en en-till-en-relation med ägaren, därför behöver de inte egna nyckelvärden eftersom främmande nyckelvärdena är unika. I föregående exempel StreetAddress behöver typen inte definiera en nyckelegenskap.

För att förstå hur EF Core spårar dessa objekt är det bra att veta att en primärnyckel skapas som en skuggegenskap för den ägda typen. Värdet för nyckeln för en instans av den ägda typen är samma som värdet för nyckeln för ägarinstansen.

Samlingar av ägda typer

För att konfigurera en samling ägda typer använder du OwnsMany i OnModelCreating.

Ägda typer behöver en primär nyckel. Om det inte finns några bra egenskaper för kandidater i .NET-typen kan EF Core försöka skapa en. Men när ägda typer definieras via en samling räcker det inte att bara skapa en skuggegenskap för att fungera både som sekundärnyckel till ägaren och den primära nyckeln för den ägda instansen, som vi gör för OwnsOne: det kan finnas flera instanser av ägd typ för varje ägare, och därför räcker inte ägarens nyckel för att tillhandahålla en unik identitet för varje ägd instans.

De två enklaste lösningarna på detta är:

  • Definiera en surrogatprimärnyckel på en ny egenskap oberoende av den referensnyckel som pekar på ägaren. De inneslutna värdena måste vara unika för alla ägare (t.ex. om Överordnade {1} har Underordnad {1}, då kan inte Överordnade {2} ha Underordnad {1}), så värdet har ingen inbyggd betydelse. Eftersom den utländska nyckeln inte är en del av primärnyckeln kan dess värden ändras, så du kan flytta ett underordnat objekt från en överordnad till en annan överordnad, men detta går oftast emot den aggregerade semantiken.
  • Använda den främmande nyckeln och en ytterligare egenskap som en sammansatt nyckel. Det ytterligare egenskapsvärdet behöver nu bara vara unikt för en viss överordnad. Till exempel, om Förälder {1} har Barn {1,1} kan Förälder {2} fortfarande ha Barn {2,1}. Genom att göra sekundärnyckeln till en del av primärnyckeln blir relationen mellan ägaren och den ägda entiteten oföränderlig och återspeglar aggregerad semantik bättre. Det här är vad EF Core gör som standard.

I det här exemplet använder vi Distributor-klassen.

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

Som standard är ShippingCenters den primära nyckeln som används för den ägda typen som refereras via navigeringsegenskapen ("DistributorId", "Id") där "DistributorId" är FK:n och "Id" är ett unikt int värde.

Så här konfigurerar du ett annat primärnyckelanrop HasKey.

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

Modellen ovan mappas till följande databasschema:

Skärmbild av databasmodellen för entitet som innehåller ägd samling

Mappa ägda datatyper med tabelluppdelning

När du använder relationsdatabaser mappas som standard referensägda typer till samma tabell som ägaren. Detta kräver att tabellen delas upp i två: vissa kolumner används för att lagra ägarens data och vissa kolumner används för att lagra data för den ägda entiteten. Det här är en vanlig funktion som kallas tabelldelning.

Som standard namnger EF Core databaskolumnerna för egenskaperna för den ägda entitetstypen enligt mönstret Navigation_OwnedEntityProperty. StreetAddress Därför visas egenskaperna i tabellen Beställningar med namnen "ShippingAddress_Street" och "ShippingAddress_City".

Du kan använda HasColumnName metoden för att byta namn på dessa kolumner.

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

Anmärkning

De flesta konfigurationsmetoder av normal entitetstyp som Ignorera kan anropas på samma sätt.

Dela samma .NET-typ mellan flera ägda typer

En ägd entitetstyp kan vara av samma .NET-typ som en annan ägd entitetstyp. Därför kanske .NET-typen inte räcker för att identifiera en ägd typ.

I dessa fall blir egenskapen som pekar från ägaren till den ägda entiteten den definierande navigeringen för den ägda entitetstypen. Ur EF Core-perspektivet är den definierande navigeringen en del av typens identitet tillsammans med .NET-typen.

I följande klass ShippingAddress och BillingAddress är båda av samma .NET-typ, StreetAddress.

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

För att förstå hur EF Core särskiljer spårade instanser av dessa objekt kan det vara bra att tänka sig att den definierande navigeringen har blivit en del av nyckeln för instansen tillsammans med värdet på ägarens nyckel och den ägda typens .NET-typ.

Kapslade ägda typer

I det här exemplet OrderDetails äger BillingAddress och ShippingAddress, som är båda StreetAddress typerna. Ägs OrderDetails av DetailedOrder-typen.

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

Varje navigering till en ägd typ definierar en separat entitetstyp med helt oberoende konfiguration.

Förutom kapslade ägda typer kan en ägd typ referera till en vanlig entitet som kan vara antingen ägare eller en annan entitet så länge den ägda entiteten är på den beroende sidan. Den här funktionen skiljer ägda entitetstyper från komplexa typer i EF6.

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

Konfigurera ägda typer

Du kan länka OwnsOne metoden i ett fluent-anrop för att konfigurera den här modellen:

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

Observera anropet WithOwner som används för att definiera navigeringsegenskapen som pekar tillbaka på ägaren. För att definiera en navigering till ägarentitetstypen som inte ingår i ägarskapsrelationen bör WithOwner() anropas utan argument.

Det är också möjligt att uppnå detta resultat med både OwnedAttributeOrderDetails och StreetAddress.

Lägg också märke till anropet Navigation. Navigeringsegenskaper för ägda typer kan konfigureras vidare på samma sätt som för icke-ägda navigeringsegenskaper.

Modellen ovan mappas till följande databasschema:

Skärmbild av databasmodellen för entitet som innehåller kapslade ägda referenser

Lagra ägda typer i separata tabeller

Till skillnad från EF6-komplexa typer kan ägda typer dessutom lagras i en separat tabell från ägaren. För att åsidosätta konventionen som mappar en ägd typ till samma tabell som ägaren kan du helt enkelt anropa ToTable och ange ett annat tabellnamn. I följande exempel mappas OrderDetails och dess två adresser till en separat tabell från DetailedOrder:

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

Det är också möjligt att använda TableAttribute för att åstadkomma detta, men observera att detta skulle misslyckas om det finns flera navigeringar till den ägda typen eftersom i så fall flera entitetstyper skulle mappas till samma tabell.

Förfrågan på ägda typer

När du frågar ägaren inkluderas de ägda typerna som standard. Det är inte nödvändigt att använda Include metoden, även om de ägda typerna lagras i en separat tabell. Baserat på den modell som beskrevs tidigare, hämtar följande fråga Order, OrderDetails och de två ägda StreetAddresses från databasen.

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

Begränsningar

Vissa av dessa begränsningar är grundläggande för hur ägda entitetstyper fungerar, men vissa andra är begränsningar som vi kanske kan ta bort i framtida versioner:

Designbegränsningar

  • Du kan inte skapa en DbSet<T> för en ägd typ.
  • Du kan inte anropa Entity<T>() med en ägd typ på ModelBuilder.
  • Instanser av ägda entitetstyper kan inte delas av flera ägare (det här är ett välkänt scenario för värdeobjekt som inte kan implementeras med hjälp av ägda entitetstyper).

Aktuella brister

  • Ägda entitetstyper kan inte ha arvshierarkier

Brister i tidigare versioner

  • I EF Core 2.x kan referensnavigeringar till ägda entitetstyper inte vara null om de inte uttryckligen mappas till en separat tabell från ägaren.
  • I EF Core 3.x markeras kolumnerna för ägda entitetstyper som mappas till samma tabell som ägaren alltid som nullbara.