从属实体类型

EF Core 使你能够对只能出现在其他实体类型的导航属性上的实体类型进行建模。 它们称为“从属实体类型”。 包含从属实体类型的实体是其所有者。

从属实体本质上是所有者的一部分,没有它就不能存在,它们在概念上类似于聚合。 这意味着,根据定义,从属实体位于与所有者关系的从属关系中。

显式配置

根据约定,EF Core 永远不会将从属实体类型包含在模型中。 可以使用 OnModelCreating 中的 OwnsOne 方法,也可以使用 OwnedAttribute 注释类型,以将类型配置为从属类型。

在此示例中,StreetAddress 是一个无标识属性的类型。 它用作 Order 类型的属性来指定特定订单的发货地址。

我们可以使用 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; }
}

还可以使用 OnModelCreating 中的 OwnsOne 方法来指定 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 所做的那样:每个所有者可以有多个从属类型实例,因此所有者的键不足以为每个从属实例提供唯一的标识。

对此,两种最直接的解决方案是:

  • 在新属性上定义独立于指向所有者的外键的代理主键。 包含的值需要在所有所有者中是唯一的(例如,如果父 {1} 具有子 {1},则父 {2} 不能具有子 {1}),因此该值没有任何固有的含义。 由于外键不是主键的一部分,因此可以更改其值,这样你可以将子项从一个父项移动到另一个父项,但这通常与聚合语义相悖。
  • 使用外键和附加属性作为组合键。 附加属性值现在只需要对于给定的父级是唯一的(因此,如果父 {1} 具有子 {1,1}则父 {2} 仍然可以具有子 {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”。

可以使用 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 类型的一部分。

例如,在下面的类中,ShippingAddressBillingAddress 都是相同的 .NET 类型 StreetAddress

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

为了了解 EF Core 将如何区分这些对象的跟踪实例,认为定义导航已与所有者的密钥值和从属类型的 .NET 类型一起成为实例密钥的一部分可能会很有用。

嵌套的从属类型

在此示例中,OrderDetails 具有 BillingAddressShippingAddress,它们都是 StreetAddress 类型。 然后 OrderDetailsDetailedOrder 类型所有。

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

配置从属类型

可以将 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() 应在没有任何参数的情况下调用。

OrderDetailsStreetAddress 上使用 OwnedAttribute 也可以达到这一结果。

此外,请注意 Navigation 调用。 在 EFCore 5.0 中,可以像配置非从属导航属性一样,进一步配置从属类型的导航属性。

以上模型映射到以下数据库架构:

包含嵌套的从属引用的实体的数据库模型的屏幕截图

将从属类型存储在单独的表中

与 EF6 复杂类型不同的是,从属类型可以存储在所有者的单独表中。 为了重写将从属类型映射到与所有者相同的表的约定,只需调用 ToTable,并提供不同的表名即可。 以下示例将 OrderDetails 及其两个地址映射到 DetailedOrder 的单独表:

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

也可以使用 TableAttribute 来完成此操作,但请注意,如果有多个到从属类型的导航,这将失败,因为在这种情况下,多个实体类型将映射到同一个表。

查询拥有的类型

查询所有者时,固有类型将默认包含在内。 不需要使用 Include 方法,即使从属类型都存储在单独的表中。 根据前面描述的模型,以下查询将从数据库中获取 OrderOrderDetails 和两个从属 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。