EF Core 可讓您建立只能出現在其他實體類型導覽屬性上的實體類型模型。 這些稱為 擁有的實體類型。 包含所擁有的實體類型的實體是其 擁有者。
擁有的實體基本上是擁有者的一部分,而且沒有它就不能存在,它們在概念上類似於 匯總。 這表示,依據定義,擁有的實體在與擁有者的關係中屬於相依的那一端。
提示
如果你正在建模一個價值物件——一個沒有自身身份的物件,例如一個或Coordinate——Address可以考慮使用複雜型別而非擁有型態。 與擁有型別不同,複雜型別具有值語意且沒有隱藏鍵,避免了許多陷阱;比較請參見 複雜類型與擁有實體類型 。
設定類型為具有所有權
在大部分的提供者中,實體類型永遠不會依慣例設定為擁有,您必須明確地使用 OwnsOne 方法、OnModelCreating 或註解類型 OwnedAttribute,才能將類型設定為擁有。 Azure Cosmos DB 提供者是例外狀況。 因為 Azure Cosmos DB 是文件資料庫,提供者預設會將所有相關實體類型設定為擁有。
在此範例中, StreetAddress 是沒有身分識別屬性的類型。 它用做訂單類型的屬性,指定特定訂單的送貨地址。
當從另一個實體類型參考時,我們可以使用 OwnedAttribute 將它視為擁有的實體:
[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; }
}
您也可以在 OwnsOne 中使用 OnModelCreating 方法來指定 ShippingAddress 屬性是 Order 實體類型的擁有實體,並視需要配置其他功能。
modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);
如果ShippingAddress屬性在Order型別中是私有的,您可以使用OwnsOne方法的字串版本:
modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");
上述模型會對應至下列資料庫架構:
如需更多內容, 請參閱完整的範例專案 。
提示
擁有的實體類型可以標示為必要,如需詳細資訊,請參閱 必要的一對一相依實體 。
隱含鍵
透過參考導覽設定 OwnsOne 或探索的擁有類型一律與擁有者有一對一關係,因此它們不需要自己的索引鍵值,因為外鍵值是唯一的。 在上述範例中,類型 StreetAddress 不需要定義索引鍵屬性。
為了瞭解 EF Core 如何追蹤這些物件,知道主鍵是建立為 被擁有的類型的隱藏屬性 會很有用。 擁有類型實例的鍵值會與擁有者實例的鍵值相同。
擁有的類型的集合
若要設定自有型別集合,請在OwnsMany中使用OnModelCreating。
擁有的類型必須有一個主鍵。 如果 .NET 類型上沒有良好的候選屬性,EF Core 可以嘗試建立一個。 不過,當擁有型別是透過集合定義時,僅僅建立一個陰影屬性來作為進入擁有者的外鍵以及擁有實例的主鍵是不夠的,就像我們對 OwnsOne 所做的那樣:每個擁有者可以有多個擁有的型別實例,因此擁有者的鍵不足以為每個擁有的實例提供唯一的身份識別。
這兩個最直接的解決方案如下:
- 在一個不依賴於指向擁有者的外鍵的新屬性上定義代理主鍵。 包含的值在所有擁有者中都必須是唯一的(例如,如果 Parent 有 Child ,則 Parent {1}{1} 不能有 Child {2}{1}),因此值沒有任何固有的意義。 由於外鍵不是主鍵的一部分,因此您可以變更其值,以便將子系從一個父系移到另一個父系。然而,這通常不符合聚合語意。
- 使用外鍵和額外的屬性作為複合鍵。 其他屬性值現在只需要對於給定的父系是唯一的(因此,如果 Parent {1} 有 Child {1,1},那麼 Parent {2} 仍然可以有 Child {2,1})。 讓主鍵的外鍵部分成為主鍵的一部分,擁有者與擁有實體之間的關聯性會變成固定的,並更能反映匯總語意。 這是EF Core 預設的用途。
在此範例中,我們將使用 類別 Distributor 。
public class Distributor
{
public int Id { get; set; }
public ICollection<StreetAddress> ShippingCenters { get; set; }
}
預設情況下,透過 ShippingCenters 導覽屬性參考的擁有型別的主鍵將會是 ("DistributorId", "Id"),其中 "DistributorId" 是作為 FK,而 "Id" 則是唯一的 int 值。
若要設置其他主鍵,請呼叫 HasKey。
modelBuilder.Entity<Distributor>().OwnsMany(
p => p.ShippingCenters, a =>
{
a.WithOwner().HasForeignKey("OwnerId");
a.Property<int>("Id");
a.HasKey("Id");
});
上述模型會對應至下列資料庫架構:
使用數據表分割對應擁有的類型
使用關係資料庫時,根據預設,被擁有的參考類型會對應至與擁有者相同的資料表。 這需要將數據表分割成兩個:某些數據行將用來儲存擁有者的數據,而某些數據行將用來儲存擁有實體的數據。 這是稱為 數據表分割的常見功能。
預設情況下,EF Core 會按照Navigation_OwnedEntityProperty的模式為擁有實體類型的屬性命名資料庫欄位。 因此, StreetAddress 屬性會出現在名稱為 『ShippingAddress_Street』 和 『ShippingAddress_City』 的 『Orders』 資料表中。
您可以使用 HasColumnName 方法來重新命名這些資料行。
modelBuilder.Entity<Order>().OwnsOne(
o => o.ShippingAddress,
sa =>
{
sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
sa.Property(p => p.City).HasColumnName("ShipsToCity");
});
注意
大部分的一般實體類型組態方法,例如 Ignore ,都可以以相同方式呼叫。
在多個擁有者類型之間共用相同的 .NET 類型
擁有的實體類型可以與另一個擁有的實體類型具有相同的 .NET 類型,因此僅靠 .NET 類型可能不足以識別一個擁有的類型。
在這些情況下,從擁有者指向擁有實體的屬性會 變成定義擁有實體類型的導覽 。 從 EF Core 的觀點來看,定義的導覽屬性是類型身分識別的一部分,與 .NET 類型一同。
例如,在下列類別 ShippingAddress 中,和 BillingAddress 都是相同 .NET 類型的 StreetAddress。
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
為了瞭解 EF Core 如何區分這些對象的追蹤實例,將定義導航視為實例鍵的一部分,與擁有者的鍵值及其擁有類型的 .NET 類型共同形成實例鍵,會是非常有幫助的。
巢狀擁有者的類型
在這個範例中,OrderDetails 擁有 BillingAddress 和 ShippingAddress,這兩者都是 StreetAddress 類型。 然後 OrderDetails 被 DetailedOrder 類型擁有。
public class DetailedOrder
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
Pending,
Shipped
}
每次導覽到一個所擁有的類型都會定義一個具有完全獨立設定的個別實體類型。
除了巢狀擁有類型外,擁有的類型亦可以參考一般實體,這個實體可以是擁有者或其他實體,但前提是該擁有實體處於依賴方。 這項功能將擁有的實體類型與 EF6 中的複雜類型區分開來。
public class OrderDetails
{
public DetailedOrder Order { get; set; }
public StreetAddress BillingAddress { get; set; }
public StreetAddress ShippingAddress { get; set; }
}
配置已擁有的類型
您可以透過 Fluent 呼叫來鏈結 OwnsOne 方法,以設定此模型:
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);
});
WithOwner請注意,用來定義指向擁有者之導覽屬性的呼叫。 若要定義指向不屬於擁有關係的實體類型的導航,應該在沒有任何自變數的情況下呼叫 WithOwner()。
您也可以在 OwnedAttribute 和OrderDetails上使用 StreetAddress 來達成此結果。
此外,請注意 Navigation 呼叫。 您可以針對擁有類型的導覽屬性,進一步設定,就像設定非擁有的導覽屬性一樣。
上述模型會對應至下列資料庫架構:
將擁有的類型儲存在不同的數據表中
與EF6複雜類型不同,擁有類型可以儲存在獨立於擁有者的資料表中。 若要覆寫將擁有類型對應至與擁有者相同數據表的慣例,您可以簡單地呼叫 ToTable 並提供另一個數據表名稱。 下列範例將 OrderDetails 及其兩個位址對應到與 DetailedOrder 不同的數據表:
modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });
您也可以使用 TableAttribute 來完成這項作業,但請注意,如果擁有的類型有多個導航,就會失敗,因為在此情況下,多個實體型別會對應至相同的數據表。
查詢擁有的類型
查詢擁有者時,預設會包含擁有的類型。 即使擁有的類型儲存在個別數據表中,也不需要使用 Include 方法。 根據先前所述的模型,下列查詢會從資料庫中取得 Order、OrderDetails 以及兩個擁有 StreetAddresses 的項目。
var order = await context.DetailedOrders.FirstAsync(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");
限制
其中一些限制對擁有的實體類型的運作方式至關重要,但有些則是我們在未來版本中可能能夠移除的限制:
設計上限制
- 您無法為擁有的類型建立
DbSet<T>。 - 您無法使用擁有類型在
Entity<T>()上呼叫ModelBuilder。 - 擁有實體類型的實例無法由多個擁有者共用(這是無法使用擁有實體類型實作之值物件的已知案例)。
目前的缺點
- 擁有的實體類型不能有繼承階層
舊版的缺點
- 在 EF Core 2.x 中,對擁有的實體類型的參考導航不能為 Null,除非它們被明確映射到與擁有者分開的數據表。
- 在EF Core 3.x 中,對應至與擁有者相同數據表之擁有實體類型的數據行一律標示為可為 Null。