《Entity Framework Core in Action》--- 读书随记(5)

Part 2 Entity Framework in depth

《Entity Framework Core in Action》
-- SECOND EDITION

Author: JON P SMITH

如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的

8 Configuring relationships

8.2 What navigational properties do you need?

实体类之间关系的配置应该以项目的业务需求为指导。您可以在关系的两端添加导航属性,但这表明每个导航属性都是有用的,而有些导航属性则不是。最好只提供从业务或软件设计角度看有意义的导航属性

8.4 Configuring relationships By Convention

在配置关系时,按惯例方法是一种真正节省时间的方法。在 EF6.x 中,我曾经费力地定义我的关系,因为我没有完全理解按惯例方法对于关系的力量。现在我了解了惯例,我让 EF Core 建立了我的大部分关系,除了按惯例不起作用的少数情况

8.4.1 What makes a class an entity class?

  1. EF Core 扫描应用程序的 DbContext,寻找任何公共 DbSet < T > 属性。它假设 DbSet < T > 属性中的类 T 是实体类
  2. EF Core 还查看步骤1中找到的类中的每个公共属性,并查看可能是导航属性的属性。其类型包含未被定义为标量属性的类的属性(string 是一个类,但它被定义为标量属性)被假定为导航属性。这些属性可以显示为单个链接(如public PriceOffer Promotion ( get; set; })或实现 IEnumable < T > 接口的类型(如public ICollection< Review > Reviews { get; set; })
  3. EF Core 检查每个实体类是否有一个主键。如果类没有一个主键,并且没有被配置为没有一个键(参见7.9.3节) ,或者如果类没有被排除在外,EF Core 将抛出一个异常

8.4.3 How EF Core finds foreign keys By Convention

外键在类型和名称上必须与主键匹配,但是为了处理一些情况,外键名称匹配有三个选项

第三个解释的有点模糊:

public class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    //------------------------------
    //Relationships
    public int? ManagerEmployeeId { get; set; }
    public Employee Manager { get; set; }
}

名为 Employee 的实体类有一个名为 Manager 的导航属性,该属性链接到雇员的经理,该经理也是一名雇员。您不能使用 EmployeeId 的外键(选项1) ,因为它已经用于主键。因此,可以使用选项3,并在开始时使用导航属性名称来调用外键 ManagerEmployeeId

8.4.4 Nullability of foreign keys: Required or optional dependent relationships

外键的可空性定义了关系是必需的(不可空的外键)还是可选的(可空的外键)。必需的关系通过确保外键链接到有效的主键来确保关系的存在

通过将外键值设置为 null,可选关系允许主体实体和依赖实体之间没有链接。Employee 实体类中的 Manager 导航属性是一个可选关系的示例,因为处于业务层次结构顶端的人不会有老板

当删除主体实体时,关系的必需或可选状态也会影响依赖实体的处理。每种关系类型的 OnDelete 操作的默认设置如下:

  • 对于必需的关系,EF Core 将 OnDelete 操作设置为 Cascade。如果主体实体被删除,依赖实体也将被删除
  • 对于可选关系,EFCore 将 OnDelete 操作设置为 ClientSetNull。如果正在跟踪依赖实体,则删除主体实体时将外键设置为 null。但是,如果没有跟踪依赖实体,则由数据库约束删除设置接管,而 ClientSetNull 设置设置数据库规则,就好像限制设置已经就位一样。结果是删除在数据库级失败,并引发异常。

8.4.5 Foreign keys: What happens if you leave them out?

如果 EF Core 通过导航属性或通过 Fluent API 配置的关系找到一个关系,它需要一个外键来在关系数据库中建立关系。在实体类中包含外键是一种很好的做法,可以更好地控制外键的可空性。另外,在处理断开连接的更新中的关系时,对外键的访问也很有用

但是如果您确实遗漏了一个外键(有意或无意) ,EFCore 配置将添加一个外键作为影子属性。在第7章中介绍的影子属性是隐藏属性,只能通过特定的 EF Core 命令访问。将外键作为影子属性自动添加会很有用。例如,我的一个客户机有一个通用的 Note 实体类,它被添加到许多实体中的 Notes 集合中

8.4.6 When does By Convention configuration not work?

如果要使用按约定配置方法,则需要知道它何时不起作用,以便可以使用其他方法来配置您的关系。以下是我列出的不适用的场景,首先列出的是最常见的场景

  • 你有复合外键
  • 您希望创建一对一的关系,而不需要双向导航链接
  • 要重写默认的删除行为设置
  • 有两个导航属性指向同一个类
  • 您需要定义特定的数据库约束

8.5 Configuring relationships by using Data Annotations

8.5.1 The ForeignKey Data Annotation

ForeignKey 数据注释允许您为类中的导航属性定义外键。以 Employee 类的分层示例为例,可以使用此注释为 Manager 导航属性定义外键。下面的清单显示了一个更新后的 Employee 实体类,该类具有一个新的、更短的 Manager 导航属性外键名,该外键名不适合按约定命名: ManagerEmployeeId

public class Employee
{
    public int EmployeeId { get; set; }
    public string Name { get; set; }
    public int? ManagerId { get; set; }
    [ForeignKey(nameof(ManagerId))]
    public Employee Manager { get; set; }
}

8.5.2 The InverseProperty Data Annotation

InverseProperty 数据注释是一种相当专业的数据注释,当两个导航属性进入同一个类时可以使用它。在这一点上,EF Core 无法计算出哪些外键与哪些导航属性相关。这种情况最好在代码中显示。下面的清单显示了一个带有两个列表的 Person 实体类示例: 一个用于图书管理员拥有的图书,另一个用于借给特定人员的 Books

public class Person
{
    public int PersonId { get; set; }
    public string Name { get; set; }

    [InverseProperty("Librarian")]
    public ICollection<LibraryBook> LibrarianBooks { get; set; }

    [InverseProperty("OnLoanTo")]
    public ICollection<LibraryBook> BooksBorrowedByMe { get; set; }
}

这段代码是您很少使用的配置选项之一,但是如果遇到这种情况,您必须使用它或者定义与 Fluent API 的关系。否则,EFCore 将在启动时抛出异常,因为它无法解决如何配置关系

8.6 Fluent API relationship configuration commands

正如我在8.4节中所说的,您可以通过使用 EF Core 的 By Convention 方法来配置大多数关系。但是,如果您想要配置一个关系,Fluent API 有一组设计良好的命令,可以覆盖所有可能的关系组合。它还有额外的命令,允许您定义其他数据库约束。下图显示了定义与 Fluent API 的关系的格式。所有 FluentAPI 关系配置命令都遵循此模式。

8.6.1 Creating a one-to-one relationship



不同的地方在最后的HasForeignkey,其实它指定了外键放在哪里了,也就是谁是主,谁是依赖

8.6.2 Creating a one-to-many relationship

一对多关系更简单,因为只有一种格式: 许多实体包含外键值。您可以通过简单地为许多实体中的外键提供遵循 By Convention 方法的名称来定义与 By Convention 方法的大多数一对多关系

出于性能方面的考虑,应该对导航集合使用 HashSet < T > ,因为它改进了 EF Core 的某些查询和更新过程。但是 HashSet 不保证条目的顺序,如果您将排序添加到包含中,可能会导致问题。这就是为什么我建议使用 ICollection < T > 来对 Include 方法进行排序,因为 ICollection 保留了添加条目的顺序。但是如果没有在 include 中使用 sort,因此可以使用 HashSet < T > 来获得更好的性能

8.6.3 Creating a many-to-many relationship

CONFIGURING A MANY-TO-MANY RELATIONSHIP USING A LINKING ENTITY CLASS

在 Book/Author 示例中,By Convention 配置可以查找并链接所有标量和导航属性,因此唯一需要的配置是设置主键

如果使用API配置:

public static void Configure (this EntityTypeBuilder<BookAuthor> entity)
{
    entity.HasKey(p => new { p.BookId, p.AuthorId });
    //-----------------------------
    //Relationships
    entity.HasOne(p => p.Book)
        .WithMany(p => p.AuthorsLink)
        .HasForeignKey(p => p.BookId);

    entity.HasOne(p => p.Author)
        .WithMany(p => p.BooksLink)
        .HasForeignKey(p => p.AuthorId);
}

CONFIGURING A MANY-TO-MANY RELATIONSHIP WITH DIRECT ACCESS TO THE OTHER ENTITY

也可以使用API配置

8.7 Controlling updates to collection navigational properties

有时,您需要控制对集合导航属性的访问。尽管可以通过将 setter 设置为私有来控制对一对一导航的访问,但是这种方法对集合不起作用,因为大多数集合类型允许您添加或删除条目。要完全控制集合导航属性,您需要使用backing fields。

对于控制集合导航属性的示例,将向 Book 类添加缓存的 ReviewsAverageVotes 属性。此属性将保持与本书链接的所有评论中的平均投票。要做到这一点,你需要

  • 添加一个名为 _ review 的 backing field 来保存 Reviews 集合,并更改属性以返回在 _ review backing field 中保存的集合的只读副本
  • 添加一个名为 ReviewsAverageVotes 的只读属性,以保存来自与本书链接的 Reviews 的缓存平均投票。
  • 添加添加评论的方法并从 _ review backing field 中删除评论。每个方法使用当前的评论列表重新计算平均票数

你不必配置这个 backing field,因为你使用的是按约定命名,默认情况下,EF Core 读写数据到 _ review 字段

8.8 Additional methods available in Fluent API relationships

我们已经介绍了配置标准关系的所有方法,但是关系中一些最详细的部分需要在关系的 Fluent API 配置中添加额外的命令。在本节中,我们将介绍四种方法,它们定义关系的一些更深层次的部分

  • OnDelete: 更改依赖实体的删除操作
  • IsRequired: 定义外键的可空性
  • HasPrincipalKey: 使用备用唯一键
  • HasConstraintName: 设置外键约束名称和对关系数据的 MetaData 访问

8.8.1 OnDelete: Changing the delete action of a dependent entity

public static void Configure (this EntityTypeBuilder<LineItem> entity)
{
    entity.HasOne(p => p.ChosenBook)
        .WithMany()
        .OnDelete(DeleteBehavior.Restrict);
}

除了上面的,还有几种:

8.8.2 IsRequired: Defining the nullability of the foreign key

8.8.3 HasPrincipalKey: Using an alternate unique key

public class Person
{
    public int PersonId { get; set; }
    public string Name { get; set; }
    public Guid UserId { get; set; }
    public ContactInfo ContactInfo { get; set; }
}
public class ContactInfo
{
    public int ContactInfoId { get; set; }
    public string MobileNumber { get; set; }
    public string LandlineNumber { get; set; }
    public Guid UserIdentifier { get; set; }
}

8.9 Alternative ways of mapping entities to database tables

8.9.1 Owned types: Adding a normal class into an entity class

EF Core 有 owned 类型,它允许您定义一个类,该类包含数据的公共分组,比如地址或审计数据,您希望在数据库的多个位置使用这些数据。拥有的类型类没有自己的主键,所以它没有自己的标识; 它依赖于“拥有”它的实体类的标识。在 DDD 术语中,拥有的类型称为值对象

两种方法使用 owned 类型:
所拥有的类型数据保存在实体类映射到的同一个表中。
拥有的类型数据保存在一个独立于实体类的表中。

OWNED TYPE DATA IS HELD IN THE SAME TABLE AS THE ENTITY CLASS

这种方式很方便,特别是在有不同的entity共享这种类型结构的时候,当然也可以在API中配置

modelBulder.Entity<OrderInfo>()
    .OwnsOne(p => p.BillingAddress);

modelBulder.Entity<OrderInfo>()
    .OwnsOne(p => p.DeliveryAddress);

OWNED TYPE DATA IS HELD IN A SEPARATE TABLE FROM THE ENTITY CLASS

EF Core 保存所拥有类型内部数据的另一种方式是在一个单独的表中,而不是在实体类中。在本例中,您将创建一个 User 实体类,该类具有一个名为 HomeAddress 的属性,类型为 Address。在这种情况下,在配置代码中 OwnsOne 方法之后添加 ToTable 方法

modelBulder.Entity<User>()
    .OwnsOne(p => p.HomeAddress);
    .ToTable("Addresses");

8.9.2 Table per hierarchy (TPH): Placing inherited classes into one table

每个层次结构(TPH)的表将相互继承的所有类存储在一个数据库表中。例如,如果希望在商店中保存付款,那么该付款可以是现金(PaymentCash)或信用卡(PaymentCard)。每个选项都包含金额(比如10美元) ,但是信用卡选项有额外的信息,比如在线交易收据。在这种情况下,TPH 使用一个表来存储继承类的所有版本,并根据保存的内容返回正确的实体类型 PaymentCash 或 PaymentCard

TPH 可以按照约定配置,它将所有继承类的版本合并到一个表中。这种方法的好处是将公共数据保存在一个表中,但是访问该数据有点麻烦,因为每个继承类型都有自己的 DbSet < T > 属性。但是,当您添加 Fluent API 时,所有继承的类都可以通过一个 DbSet < T > 属性访问,在我们的示例中,这使得 PaymentCash/PaymentCard 示例更加有用

CONFIGURING TPH BY CONVENTION

public class PaymentCash
{
    [Key]
    public int PaymentId { get; set; }
    public decimal Amount { get; set; }
}

public class PaymentCard : PaymentCash
{
    public string ReceiptCode { get; set; }
}

//Table-per-hierarchy
public DbSet<PaymentCash> CashPayments { get; set; }
public DbSet<PaymentCard> CreditPayments { get; set; }

使用 By Convention 方法,显示了应用程序的 DbContext,其中有两个 DbSet < T > 属性,每个属性对应两个类。因为包含了这两个类,并且 PaymentCard 继承自 PaymentCash,所以 EF Core 将在一个表中存储这两个类

USING THE FLUENT API TO IMPROVE OUR TPH EXAMPLE

尽管 By Convention 方法减少了数据库中的表数量,但是您有两个独立的 DbSet < T > 属性,并且您需要使用正确的属性来查找所使用的付款。此外,您还没有可以在任何其他实体类中使用的通用支付类。但是通过重新安排和添加一些 FluentAPI 配置,您可以使这个解决方案更加有用

除了实体类之外,您还需要更改应用程序的 DbContext 并添加一些 Fluent API 配置来告诉 EF Core 您的 TPH 类,因为它们不再适合 By Convention 方法

ACCESSING TPH ENTITIES

当您查询 TPH 数据时,EF Core OfType < T > 方法允许您过滤数据以找到特定的类。例如,查询 context.Payments.OfType< PaymentCard >()将仅返回使用信用卡的支付。您也可以在包含中过滤 TPH 类

8.9.3 Table per Type (TPT): Each class has its own table

EF Core 5版本添加了 table per type (TPT)选项,允许从基类继承的每个实体类拥有自己的表。此选项与上文中介绍的每个层次结构(TPH)方法的表相反。如果继承层次结构中的每个类都有很多不同的信息,TPT 是一个很好的解决方案; 如果每个继承类都有很大的公共部分,而且每个类只有少量的数据,那么 TPH 更好。

一个DbSet,可以访问所有子类型

public DbSet<Container> Containers { get; set; }

modelBuilder.Entity<ShippingContainer>()
    .ToTable(nameof(ShippingContainer));
modelBuilder.Entity<PlasticContainer>()
    .ToTable(nameof(PlasticContainer));

有几个选项可以加载一种类型的 TPT 类。这里有三种方法,最有效的在最后:

  • Read all query -- context.Containers.ToList()
  • OfType query -- context.Containers.OfType< ShippingContainer >().ToList()
  • Set query -- context.Set< ShippingContainer >().ToList()

8.9.4 Table splitting: Mapping multiple entity classes to the same table

下一个特性称为表分割,它允许您将多个实体映射到同一个表。如果要为一个实体存储大量数据,但是对该实体的常规查询只需要几列,则此特性非常有用。分割表就像在实体类中构建一个 Select 查询; 查询会更快,因为只加载整个实体数据的一部分。它还可以通过将表分割到两个或多个类来加快更新速度


将这两个实体配置为表分割后,可以单独查询 BookSummary 实体并获取摘要部分。要获取 BookDetails 部分,可以查询 BookSummary 实体并同时加载 Details 关系属性(比如,使用 Include 方法) ,也可以直接从数据库读取 BookDetails 部分

  • 可以单独更新表拆分中的单个实体类; 不必加载表拆分中涉及的所有实体来执行更新
  • 您已经看到一个表被分割为两个实体类,但是您可以对任意数量的实体类进行表分割
posted @   huang1993  阅读(74)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示