培训
模块
使用 EF Core 持久保存和检索关系数据 - Training
本模块指导你完成创建数据访问项目的步骤。 你将使用 Entity Framework Core (EF Core) 连接到关系数据库并构造创建、读取、更新和删除 (CRUD) 查询。
EF Core 使你能够对只能出现在其他实体类型的导航属性上的实体类型进行建模。 它们称为“从属实体类型”。 包含从属实体类型的实体是其所有者。
从属实体本质上是所有者的一部分,没有它就不能存在,它们在概念上类似于聚合。 这意味着,根据定义,从属实体位于与所有者关系的从属关系中。
在大多数提供程序中,实体类型永远不会按约定配置为已拥有,必须显式使用 OnModelCreating
中的 OwnsOne
方法或使用 OwnedAttribute
为类型做注释以将类型配置为已拥有。 Azure Cosmos DB 提供程序是此情况的一个例外。 由于 Azure Cosmos DB 是文档数据库,因此提供程序在默认情况下将所有相关实体类型配置为已拥有。
在此示例中,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
所做的那样:每个所有者可以有多个从属类型实例,因此所有者的键不足以为每个从属实例提供唯一的标识。
对此,两种最直接的解决方案是:
在本示例中,我们使用 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 类型可能不足以标识从属类型。
在这些情况下,从所有者指向从属实体的属性将成为从属实体类型的定义导航。 从 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; }
}
可以将 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()
应在没有任何参数的情况下调用。
在 OrderDetails
和 StreetAddress
上使用 OwnedAttribute
也可以达到这一结果。
此外,请注意 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 持久保存和检索关系数据 - Training
本模块指导你完成创建数据访问项目的步骤。 你将使用 Entity Framework Core (EF Core) 连接到关系数据库并构造创建、读取、更新和删除 (CRUD) 查询。