从属实体类型Owned Entity Types

EF Core 允许您为只能出现在其他实体类型的导航属性中的实体类型建模。EF Core allows you to model entity types that can only ever appear on navigation properties of other entity types. 它们称为 拥有的实体类型These are called owned entity types. 包含拥有的实体类型的实体是其 所有者The entity containing an owned entity type is its owner.

拥有的实体实质上是所有者的一部分,并且在没有它的情况下不存在,它们在概念上类似于 聚合Owned entities are essentially a part of the owner and cannot exist without it, they are conceptually similar to aggregates. 这意味着,拥有的实体由与所有者的关系的从属方定义。This means that the owned entity is by definition on the dependent side of the relationship with the owner.

显式配置Explicit configuration

所有实体类型永远不会通过约定 EF Core 在模型中。Owned entity types are never included by EF Core in the model by convention. 您可以使用 OwnsOne 中的方法 OnModelCreating 或使用 OwnedAttribute EF Core 2.1) 中的新 (来批注该类型,以便将该类型配置为拥有的类型。You can use the OwnsOne method in OnModelCreating or annotate the type with OwnedAttribute (new in EF Core 2.1) to configure the type as an owned type.

在此示例中, StreetAddress 是一个无标识属性的类型。In this example, StreetAddress is a type with no identity property. 它用作 Order 类型的属性来指定特定订单的发货地址。It is used as a property of the Order type to specify the shipping address for a particular order.

OwnedAttribute从另一个实体类型引用时,可以使用将其视为拥有的实体:We can use the OwnedAttribute to treat it as an owned entity when referenced from another entity type:

[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 ,并根据需要配置其他方面。It is also possible to use the OwnsOne method in OnModelCreating to specify that the ShippingAddress property is an Owned Entity of the Order entity type and to configure additional facets if needed.

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

如果该 ShippingAddress 属性是类型中的私有属性 Order ,则可以使用该方法的字符串版本 OwnsOneIf the ShippingAddress property is private in the Order type, you can use the string version of the OwnsOne method:

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

有关更多上下文,请参阅 完整的示例项目See the full sample project for more context.

隐式键Implicit keys

OwnsOne通过引用导航配置的或通过引用导航发现的拥有的类型与所有者始终具有一对一的关系,因此,它们不需要其自己的键值,因为外键值是唯一的。Owned types configured with OwnsOne or discovered through a reference navigation always have a one-to-one relationship with the owner, therefore they don't need their own key values as the foreign key values are unique. 在上面的示例中, StreetAddress 类型不需要定义键属性。In the previous example, the StreetAddress type does not need to define a key property.

为了理解 EF Core 如何跟踪这些对象,知道主键是作为所属类型的 影子属性 创建的,这会很有用。In order to understand how EF Core tracks these objects, it is useful to know that a primary key is created as a shadow property for the owned type. 所拥有类型的实例的键值将与所有者实例的键的值相同。The value of the key of an instance of the owned type will be the same as the value of the key of the owner instance.

拥有的类型的集合Collections of owned types

备注

此为 EF Core 2.2 中的新增功能。This feature is new in EF Core 2.2.

若要配置拥有类型的集合,请使用 OwnsMany 中的 OnModelCreatingTo configure a collection of owned types use OwnsMany in OnModelCreating.

拥有的类型需要主键。Owned types need a primary key. 如果 .NET 类型上没有合适的候选属性,EF Core 可以尝试创建一个。If there are no good candidates properties on the .NET type, EF Core can try to create one. 但是,当所有类型是通过集合定义的时,只需创建一个影子属性以同时充当所有者的外键和该拥有实例的主键,就像我们所做的那样 OwnsOne :对于每个所有者,可以有多个拥有的类型实例,因此,所有者的密钥不足以为每个拥有的实例提供唯一标识。However, when owned types are defined through a collection, it isn't enough to just create a shadow property to act as both the foreign key into the owner and the primary key of the owned instance, as we do for OwnsOne: there can be multiple owned type instances for each owner, and hence the key of the owner isn't enough to provide a unique identity for each owned instance.

这两个最直接的解决方案是:The two most straightforward solutions to this are:

  • 在独立于指向所有者的外键的新属性上定义代理项主键。Defining a surrogate primary key on a new property independent of the foreign key that points to the owner. 所有所有者都需要唯一的包含值 (例如,如果父项 {1} 具有子级 {1} ,则父项 {2} 不能具有子 {1}) ,因此,该值没有任何固有含义。The contained values would need to be unique across all owners (e.g. if Parent {1} has Child {1}, then Parent {2} cannot have Child {1}), so the value doesn't have any inherent meaning. 由于外键不是主键的一部分,因此可以更改其值,因此,您可以将子级从一个父级移到另一个父级,但这通常会针对聚合语义进行。Since the foreign key is not part of the primary key its values can be changed, so you could move a child from one parent to another one, however this usually goes against aggregate semantics.
  • 使用外键和附加属性作为组合键。Using the foreign key and an additional property as a composite key. 现在,附加属性值只需对于给定父 (是唯一的,因此,如果 Parent {1} 具有子级, {1,1} 则父项 {2} 仍可以具有子 {2,1}) 。The additional property value now only needs to be unique for a given parent (so if Parent {1} has Child {1,1} then Parent {2} can still have Child {2,1}). 通过创建主键的外键部分,所有者和拥有的实体之间的关系将变为不可变的,并且更好地反映了聚合语义。By making the foreign key part of the primary key the relationship between the owner and the owned entity becomes immutable and reflects aggregate semantics better. 这是 EF Core 默认情况下执行的操作。This is what EF Core does by default.

在此示例中,我们将使用 Distributor 类:In this example we'll use the Distributor class:

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

默认情况下,在通过导航属性引用的所拥有的类型中使用的主键 ShippingCenters 将是 ("DistributorId", "Id") "DistributorId" FK,而 "Id" 是唯一 int 值。By default the primary key used for the owned type referenced through the ShippingCenters navigation property will be ("DistributorId", "Id") where "DistributorId" is the FK and "Id" is a unique int value.

若要配置其他 PK 调用 HasKeyTo configure a different PK call HasKey:

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

备注

在 EF Core 3.0 WithOwner() 方法不存在之前,应删除此调用。Before EF Core 3.0 WithOwner() method didn't exist so this call should be removed. 此外,不会自动发现主键,因此始终必须指定它。Also the primary key was not discovered automatically so it always had to be specified.

将拥有的类型映射到表拆分Mapping owned types with table splitting

使用关系数据库时,默认情况下,引用拥有的类型将映射到与所有者相同的表。When using relational databases, by default reference owned types are mapped to the same table as the owner. 这需要将表拆分为两个:某些列将用于存储所有者的数据,某些列将用于存储拥有实体的数据。This requires splitting the table in two: some columns will be used to store the data of the owner, and some columns will be used to store data of the owned entity. 这是一种称为 表拆分的常见功能。This is a common feature known as table splitting.

默认情况下,EF Core 会按照模式 _Navigation_OwnedEntityProperty_为拥有的实体类型的属性命名数据库列。By default, EF Core will name the database columns for the properties of the owned entity type following the pattern Navigation_OwnedEntityProperty. 因此,这些 StreetAddress 属性将显示在 "Orders" 表中,名称为 "ShippingAddress_Street" 和 "ShippingAddress_City"。Therefore the StreetAddress properties will appear in the 'Orders' table with the names 'ShippingAddress_Street' and 'ShippingAddress_City'.

您可以使用 HasColumnName 方法重命名这些列:You can use the HasColumnName method to rename those columns:

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

备注

大多数正常的实体类型配置方法(如 Ignore )都可以通过相同的方式进行调用。Most of the normal entity type configuration methods like Ignore can be called in the same way.

在多个所拥有的类型之间共享相同的 .NET 类型Sharing the same .NET type among multiple owned types

一个拥有的实体类型可以是与另一个拥有的实体类型相同的 .NET 类型,因此,.NET 类型可能不足以标识某个所有者的类型。An owned entity type can be of the same .NET type as another owned entity type, therefore the .NET type may not be enough to identify an owned type.

在这些情况下,从所有者指向拥有的实体的属性将成为所拥有实体类型的 定义导航In those cases, the property pointing from the owner to the owned entity becomes the defining navigation of the owned entity type. 从 EF Core 的角度来看,定义导航是类型标识与 .NET 类型的一部分。From the perspective of EF Core, the defining navigation is part of the type's identity alongside the .NET type.

例如,在下面的类中, ShippingAddressBillingAddress 都是相同的 .net 类型 StreetAddressFor example, in the following class ShippingAddress and BillingAddress are both of the same .NET type, StreetAddress:

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

为了理解 EF Core 如何区分跟踪的这些对象的实例,可能会认为定义导航已成为实例的键的一部分,以及所有者的键的值和拥有的类型的 .NET 类型。In order to understand how EF Core will distinguish tracked instances of these objects, it may be useful to think that the defining navigation has become part of the key of the instance alongside the value of the key of the owner and the .NET type of the owned type.

嵌套的所属类型Nested owned types

在此示例 OrderDetailsBillingAddress ,拥有和 ShippingAddress ,这两种类型都是 StreetAddressIn this example OrderDetails owns BillingAddress and ShippingAddress, which are both StreetAddress types. 然后 OrderDetailsDetailedOrder 类型所有。Then OrderDetails is owned by the DetailedOrder type.

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

每个指向所拥有的类型的导航都定义一个具有完全独立配置的单独实体类型。Each navigation to an owned type defines a separate entity type with completely independent configuration.

除了嵌套的类型外,拥有的类型还可以引用常规实体,该实体可以是所有者,也可以是其他实体,前提是拥有的实体在依赖方。In addition to nested owned types, an owned type can reference a regular entity which can be either the owner or a different entity as long as the owned entity is on the dependent side. 此功能在 EF6 中除复杂类型外,会设置拥有的实体类型。This capability sets owned entity types apart from complex types in EF6.

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

配置拥有类型Configuring Owned Types

可以 OwnsOne 通过在流畅调用中将方法链接在一起来配置此模型:It is possible to chain the OwnsOne method in a fluent call to configure this model:

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 用于定义在所有者返回导航属性的调用。Notice the WithOwner call used to define the navigation property pointing back at the owner. 若要定义对不属于所有权关系的所有者实体类型的导航, WithOwner() 应不使用任何参数调用。To define a navigation to the owner entity type that's not part of the ownership relationship WithOwner() should be called without any arguments.

还可以在和上使用来实现此结果 OwnedAttribute OrderDetails StreetAddressIt is also possible to achieve this result using OwnedAttribute on both OrderDetails and StreetAddress.

此外,请注意 Navigation 调用。In addition, notice the Navigation call. 在 EFCore 5.0 中,可以将导航属性进一步配置为 非拥有的导航属性In EFCore 5.0, navigation properties to owned types can be further configured as for non-owned navigation properties.

将拥有的类型存储在单独的表中Storing owned types in separate tables

与 EF6 复杂类型不同的是,拥有的类型可以存储在所有者的单独表中。Also unlike EF6 complex types, owned types can be stored in a separate table from the owner. 若要重写将拥有的类型映射到与所有者相同的表的约定,可以直接调用 ToTable 并提供不同的表名。In order to override the convention that maps an owned type to the same table as the owner, you can simply call ToTable and provide a different table name. 下面的示例将映射以下 OrderDetails 两个地址,并将其作为一个单独的表 DetailedOrderThe following example will map OrderDetails and its two addresses to a separate table from DetailedOrder:

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

还可以使用 TableAttribute 来实现此目的,但请注意,如果有多个导航到拥有的类型,则这会失败,因为在这种情况下,多个实体类型会映射到同一个表。It is also possible to use the TableAttribute to accomplish this, but note that this would fail if there are multiple navigations to the owned type since in that case multiple entity types would be mapped to the same table.

查询拥有的类型Querying owned types

查询所有者时,固有类型将默认包含在内。When querying the owner the owned types will be included by default. 不需要使用 Include 方法,即使所有类型都存储在单独的表中。It is not necessary to use the Include method, even if the owned types are stored in a separate table. 根据前面所述的模型,下面的查询将获得 Order ,其中 OrderDetails 两个数据库都拥有 StreetAddressesBased on the model described before, the following query will get Order, OrderDetails and the two owned StreetAddresses from the database:

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

限制Limitations

其中一些限制对于拥有的实体类型的工作方式很重要,但其他一些限制是我们可以在未来版本中删除的限制:Some of these limitations are fundamental to how owned entity types work, but some others are restrictions that we may be able to remove in future releases:

按设计限制By-design restrictions

  • 不能 DbSet<T> 为拥有的类型创建You cannot create a DbSet<T> for an owned type
  • 不能 Entity<T>() 对拥有的类型调用 ModelBuilderYou cannot call Entity<T>() with an owned type on ModelBuilder

当前缺陷Current shortcomings

  • 拥有的实体类型不能具有继承层次结构Owned entity types cannot have inheritance hierarchies
  • 引用导航到拥有的实体类型不能为 null,除非它们显式映射到与所有者不同的表Reference navigations to owned entity types cannot be null unless they are explicitly mapped to a separate table from the owner
  • 多个所有者不能共享拥有的实体类型的实例 (这是一个已知的值对象方案,不能使用拥有的实体类型实现) Instances of owned entity types cannot be shared by multiple owners (this is a well-known scenario for value objects that cannot be implemented using owned entity types)

以前版本中的缺点Shortcomings in previous versions

  • 在 EF Core 2.0 中,在派生的实体类型中不能声明导航到拥有的实体类型,除非将拥有的实体显式映射到所有者层次结构中的单独表。In EF Core 2.0, navigations to owned entity types cannot be declared in derived entity types unless the owned entities are explicitly mapped to a separate table from the owner hierarchy. 此限制已在 EF Core 2.1 中被删除This limitation has been removed in EF Core 2.1
  • 在 EF Core 仅支持2.0 和2.1 的引用导航到拥有的类型。In EF Core 2.0 and 2.1 only reference navigations to owned types were supported. 此限制已在 EF Core 2.2 中被删除This limitation has been removed in EF Core 2.2