擁有的實體類型
EF Core 可讓您建立只能出現在其他實體類型導覽屬性上的實體類型模型。 這些稱為 擁有的實體類型。 包含擁有實體類型的實體是其 擁有者。
擁有的實體基本上是擁有者的一部分,而且沒有它就不能存在,它們在概念上類似於 匯總。 這表示擁有的實體是依定義在與擁有者關聯性的相依端。
將類型設定為擁有
在大部分的提供者中,實體類型永遠不會設定為慣例所擁有 - 您必須明確地使用 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
中的 方法來指定 ShippingAddress
屬性是實體類型的擁有實體Order
,並視需要OnModelCreating
設定其他 Facet。
modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);
ShippingAddress
如果 屬性在 型別中是私用的Order
,您可以使用 方法的OwnsOne
字串版本:
modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");
上述模型會對應至下列資料庫架構:
如需更多內容, 請參閱完整的範例專案 。
提示
擁有的實體類型可以標示為必要,如需詳細資訊,請參閱 必要的一對一相依專案 。
隱含索引鍵
透過參考導覽設定 OwnsOne
或探索的擁有類型一律與擁有者有一對一關係,因此它們不需要自己的索引鍵值,因為外鍵值是唯一的。 在上述範例中 StreetAddress
,類型不需要定義索引鍵屬性。
為了瞭解EF Core 如何追蹤這些物件,知道主鍵是建立為 所擁有類型的陰影屬性 會很有用。 擁有類型的實例索引鍵值會與擁有者實例的索引鍵值相同。
擁有類型的集合
若要設定 中OnModelCreating
所使用的自有型別OwnsMany
集合。
擁有的類型需要主鍵。 如果 .NET 類型上沒有良好的候選屬性,EF Core 可以嘗試建立一個。 不過,當擁有的類型是透過集合定義時,就不足以建立陰影屬性來同時作為擁有者和擁有實例的主鍵,就像我們所做的 OwnsOne
一樣:每個擁有者可以有多個擁有的類型實例,因此擁有者的索引鍵不足以為每個擁有的實例提供唯一的身分識別。
這兩個最直接的解決方案如下:
- 在與指向擁有者之外鍵無關的新屬性上定義 Surrogate 主鍵。 包含的值在所有擁有者中都必須是唯一的(例如,如果 Parent 有 Child ,則 Parent {1} {2} 不能有 Child {1}{1}),因此值沒有任何固有的意義。 由於外鍵不是主鍵的一部分,因此您可以變更其值,因此您可以將子系從一個父系移到另一個父系,不過這通常不符合匯總語意。
- 使用外鍵和額外的屬性做為複合索引鍵。 其他屬性值現在只需要為指定的父系是唯一的(因此,如果 Parent 有 Child,則 Parent {1} {2} 仍然可以有 Child {1,1} {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
。 然後 DetailedOrder
類型擁有 OrderDetails
。
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
和StreetAddress
上使用 OrderDetails
來達成此結果。
此外,請注意 Navigation
呼叫。 您可以針對非擁有的導覽屬性,進一步設定 為擁有類型的導覽屬性。
上述模型會對應至下列資料庫架構:
將擁有的類型儲存在不同的數據表中
與EF6複雜類型不同,擁有的類型也可以儲存在與擁有者的個別數據表中。 若要覆寫將擁有類型對應至擁有者相同數據表的慣例,您可以直接呼叫 ToTable
並提供不同的數據表名稱。 下列範例會將 及其兩個位址對應 OrderDetails
至 不同的數據表 DetailedOrder
:
modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });
您也可以使用 TableAttribute
來完成這項作業,但請注意,如果擁有的型別有多個流覽,就會失敗,因為在此情況下,多個實體類型會對應至相同的數據表。
查詢擁有的類型
查詢擁有者時,預設會包含擁有的類型。 即使擁有的類型儲存在個別數據表中,也不需要使用 Include
方法。 根據先前所述的模型,下列查詢會取得 Order
, OrderDetails
以及資料庫中擁有 StreetAddresses
的兩個:
var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");
限制
其中一些限制對擁有的實體類型的運作方式至關重要,但有些則是我們在未來版本中可能能夠移除的限制:
依設計限制
- 您無法為擁有的類型建立
DbSet<T>
。 - 您無法在上使用
ModelBuilder
擁有的類型呼叫Entity<T>()
。 - 擁有實體類型的實例無法由多個擁有者共用(這是無法使用擁有實體類型實作之值物件的已知案例)。
目前的缺點
- 擁有的實體類型不能有繼承階層
舊版的缺點
- 在 EF Core 2.x 參考中,除非實體類型明確對應到擁有者的個別數據表,否則無法為 Null。
- 在EF Core 3.x 中,對應至與擁有者相同數據表之擁有實體類型的數據行一律標示為可為 Null。