《Entity Framework Core in Action》--- 读书随记(4)
Part 2 Entity Framework in depth
《Entity Framework Core in Action》
-- SECOND EDITIONAuthor: JON P SMITH
如果需要电子书的小伙伴,可以留下邮箱,看到了会发送的
7 Configuring nonrelational properties
本章通常介绍如何配置 EF Core,但是主要介绍如何在实体类中配置非关系属性
如何配置 .NET 类及其相关数据库表之间的映射,并使用诸如设置表中列的名称、 SQL 类型和可空性等特性
本章还介绍了 EF Core 的三个特性: value converters, shadow properties, backing fields
7.1 Three ways of configuring EF Core
- By Convention: 当您遵循简单的属性类型和名称的规则,EF 核心将自动配置许多软件和数据库功能。按约定的方法快速而简单,但它不能处理所有可能发生的情况
- Data Annotations: 可以向实体类和/或属性添加一系列称为数据注释的 .NET 属性,以提供额外的配置信息
- Fluent API: EF Core 有一个名为 OnModelCreate 的方法,该方法在首次使用 EF 上下文时运行。您可以覆盖此方法并添加命令(称为 FluentAPI) ,以便在 EFCore 的建模阶段向其提供额外的信息。FluentAPI 是最全面的配置信息形式,有些特性只能通过该 API 获得
7.3 Configuring by convention
By Convention 方法依赖于开发人员使用 By Convention 命名标准和类型映射,这允许 EF Core 查找和配置实体类及其关系,并定义大部分数据库模型
7.3.1 Conventions for entity classes
对于 entity 类,有如下约定:
- 必须是 public 的
- 不可以是 static 的 class
- 必须有 EF Core 可以使用的 构造函数。默认情况,无参构造函数和有参构造函数都可以
7.3.2 Conventions for parameters in an entity class
按照约定,EF Core 会查找类中具有 public 的属性,而且需要有 public 的getter 和 public, internal, protected, 或者 private 的setter
EF Core 也可以处理只读的属性,但是需要用 Fluent API
7.3.3 Conventions for name, type, and size
- 属性的名字默认是数据库表字段的名字
- .NET类型会转换为SQL中的类型,一些基本的类型是可以是实现一对一的转换的,这些类型基本是.NET中的原始类型,还有一些特殊的比如string、Datetime、Guid也可以
- 字段的长度就是.NET类型的长度,比如一个int是32bit,那么在数据库中也是32bit
7.3.4 By convention, the nullability of a property is based on .NET type
在关系数据库中,NULL代表的是 missing 或者 unknown 的数据,是否是NULL又.NET类型来决定:
- 如果类型是string,字段可以是NULL
- 原始类型或者值类型,默认是不可以NULL
- 如果原始类型或者值类型有后缀 ?或者 范型Nullable< T >,那么数据库类型可以是NULL
7.3.5 An EF Core naming convention identifies primary keys
关于数据库表主键的约定规则:
- EF Core 期望拥有一个主键属性。(按约定方法不处理由多个属性/列(称为组合键)组成的键。)
- 属性命名约定是 < class name >id 或者 Id
- 属性的类型定义了向键分配唯一值的内容
比起短名称 Id, 更加推荐长命名,这样在代码中想要表达的意思更加显而易见
7.4 Configuring via Data Annotations
7.4.1 Using annotations from System.ComponentModel.DataAnnotations
这个命名空间下的注解主要用于前台使用,比如ASP.NET,不过EF Core也使用了一些来创建模型映射,比如[Required] 和 [MaxLength]就是主要使用的。
7.4.2 Using annotations from System.ComponentModel.DataAnnotations.Schema
System.ComponentModel.DataAnnotations.Schema 命名空间中的属性更特定于数据库配置
EF Core 使用它的属性,比如[ Table ]、[ Column ]等,来设置表名和列名/类型
7.5 Configuring via the Fluent API
但是在定义 Fluent API 关系命令之前,我想介绍一种不同的方法,它将 Fluent API 命令分隔为每个实体类大小的组。这种方法非常有用,因为随着应用程序的增长,将所有 Fluent API 命令放入 OnModelCreate 方法会使寻找特定的 Fluent API 变得非常困难。解决方案是将实体类的 Fluent API 移动到一个单独的配置类中,然后从 OnModelCreate 方法调用该配置类
EF Core 以 IEntityTypeConfiguration < T > 接口的形式提供了一种方法来促进这个过程。这种方法的好处是,实体类的 Fluent API 都在一个地方,而不是与其他实体类的 Fluent API 命令混合在一起
我列出了每个独立的 modelBuilder.ApplyConfiguration 调用,以便您可以看到它们的运行情况。但是一个名为 ApplicyConfigurationsFromAssembly 的节省时间的方法可以找到所有继承IEntityTypeConfiguration < T > 的配置类并为您运行它们
modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
当应用程序首次访问应用程序的 DbContext 时调用 OnModelCreate。在这个阶段,EF Core 通过使用所有三种方法进行配置: 通过约定、数据注释和在 OnModelCreate 方法中添加的任何 Fluent API
What if Data Annotations and the Fluent API say different things?
数据注释和 FluentAPI 建模方法总是覆盖基于约定的建模。但是,如果数据注释和 Fluent API 都提供了相同属性和设置的映射,会发生什么情况呢?
我尝试通过数据注释和 Fluent API 将 WebUrl 属性的 SQL 类型和长度设置为不同的值。使用 Fluent API 值。这个测试并不是一个决定性的测试,但是 Fluent API 作为最终的仲裁者是有意义的
7.6 Excluding properties and classes from the database
第7.3.2节描述了 EF Core 如何查找属性。但有时,您会希望将实体类中的数据排除在数据库中。例如,您可能希望在类实例的生存期内使用本地数据进行计算,但是不希望将其保存到数据库中。可以通过两种方式排除类或属性: 通过数据注释或通过 FluentAPI
7.6.1 Excluding a class or property via Data Annotations
[NotMapped]可以应用在属性或者类上,用以将其排除在数据库映射上
[NotMapped]
public string LocalString { get; set; }
// or
[NotMapped]
public class ExcludeClass
{
public int LocalInt { get; set; }
}
7.6.2 Excluding a class or property via the Fluent API
modelBuilder.Entity<MyEntityClass>().Ignore(b => b.LocalString);
modelBuilder.Ignore<ExcludeClass>();
7.7 Setting database column type, size, and nullability
某些特定的 SQL 类型有自己的 FluentAPI 命令:
- IsUnicode(false) -- 将 SQL 类型设置为 varchar (nnn)(1字节字符,称为 ASCII) ,而不是 nvarchar (nnn)(2字节字符,称为 Unicode)的默认值
- HasPrecision(precision, scale) -- 设置位数(精度参数)和小数点后面的位数(比例参数)
- HasCollation(“collation name”) -- 允许您定义属性的排序规则ーー即字符和字符串类型的排序规则、大小写和重音敏感性属性
我建议使用 IsUnicode (false)方法告诉 EF Core 字符串属性只包含单字节 ASCII 格式的字符,因为使用 IsUnicode 方法可以分别设置字符串大小
7.8 Value conversions: Changing data to/from the database
EF Core 的值转换特性允许您在向数据库读写属性时更改数据:
- 将 Enum 类型属性保存为字符串(而不是数字) ,以便在查看数据库中的数据时更容易理解
- 修正从数据库读回时 dateTime 丢失其 UTC (协调世界时)设置的问题
- (高级)加密写入数据库的属性,并在读回时解密
值转换的第一个示例处理 SQL 数据库在存储 DateTime 类型方面的限制,因为它没有保存 DateTime 结构中告诉我们 DateTime 是本地时间还是 UTC 的 DateTimeKind 部分
事实上,有一种值转换器非常流行,它有一个预定义的 Fluent API 方法或属性ーー一种将 Enum 作为字符串存储在数据库中的转换
modelBuilder.Entity<ValueConversionExample>()
.Property(e => e.Stage)
.HasConversion<string>();
以下是使用值转换的一些规则和限制:
- 空值永远不会传递给值转换器。您需要编写一个值转换器来仅处理非空值
- 注意包含对转换后的值进行排序的查询。例如,如果将 Enums 转换为字符串,排序将按 Enum 名称排序,而不是按 Enum 值排序
- 转换器只能将单个属性映射到数据库中的单个列
- 您可以创建一些复杂的值转换器,比如将 int 列表序列化为 JSON 字符串。此时,EF Core 无法将 List < int > 属性与数据库中的 JSON 进行比较,因此它不会更新数据库。为了解决这个问题,您需要添加所谓的值比较器
7.9 The different ways of configuring the primary key
7.9.1 Configuring a primary key via Data Annotations
private class SomeEntity
{
[Key]
public int NonStandardKeyName { get; set; }
public string MyString { get; set; }
}
注意,[ Key ]属性不能用于复合键。在早期版本的 EF Core 中,您可以使用[ Key ]和[ Column ]属性来定义组合键,但是这个特性已经被删除了
7.9.2 Configuring a primary key via the Fluent API
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<SomeEntity>()
.HasKey(x => x.NonStandardKeyName);
modelBuilder.Entity<BookAuthor>()
.HasKey(x => new {x.BookId, x.AuthorId});
//… other configuration settings removed
}
7.9.3 Configuring an entity as read-only
在某些高级情况下,您的实体类可能没有主键
- 您希望将实体类定义为只读。如果一个实体类没有主键,那么 EF Core 将把它当作只读的
- 要将实体类映射到只读 SQL 视图。SQL 视图是工作方式类似于 SQL 表的 SQL 查询
- 您希望使用 ToSqlQuery FluentAPI 命令将实体类映射到 SQL 查询。ToSqlQuery 方法允许您定义一个 SQL 命令字符串,该命令字符串将在读入该实体类时执行
要将实体类显式设置为只读,可以使用 FluentAPI HasNoKey ()命令或将属性[ Keyless ]应用于实体类。如果实体类没有主键,则必须使用这两种方法中的任何一种将其标记为只读。任何通过没有主键的实体类更改数据库的尝试都将失败,并且会出现异常。EF Core 之所以这样做,是因为它不能在没有键的情况下执行更新,这是将实体类定义为只读的一种方法。将实体标记为只读的另一种方法是使用连贯 API 方法 ToView (“ ViewNameString”)命令将实体映射到 SQL View
如果要将实体类映射到可更新的视图,则使用/n可以更新的 SQL 视图ーー应改为使用 ToTable 命令
7.10 Adding indexes to database columns
7.11 Configuring the naming on the database side
如果您正在构建一个新的数据库,那么使用数据库各个部分的默认名称就可以了。但是如果你有一个已经存在的数据库,或者如果你的数据库需要被一个你不能改变的现有系统访问,你很可能需要为数据库的 schema 名、表名和列名使用特定的名称
7.11.1 Configuring table names
按照约定,表的名称由应用程序的 DbContext 中的 DbSet < T > 属性的名称设置,或者如果没有定义 DbSet < T > 属性,则表使用类名
[Table("XXX")]
public class Book {}
// or
modelBuilder.Entity<Book>().ToTable("XXX");
7.11.2 Configuring the schema name and schema groupings
有些数据库(如 SQLServer)允许您使用所谓的 schema 名称对表进行分组。你可以有两个名字相同但 schema 不同的表: 例如,一个名为 Books 的 schema 名显示的表与一个名为 Books 的 schema 名为 Order 的表是不同的
按照惯例,schema 名由数据库提供程序设置,因为有些数据库,如 SQLite 和 MySQL,不支持模式。对于支持模式的 SQL Server,默认的 schema 名称是 dbo,这是 SQL Server 的默认名称。只能通过 Fluent API 更改默认的 schema 名称
7.11.3 Configuring the database column names in a table
7.14 Shadow properties: Hiding column data inside EF Core
影子属性允许您访问数据库列,而无需将它们作为属性显示在实体类中。影子属性允许您“隐藏”您认为不属于实体类正常使用的数据。这都是关于良好的软件实践: 您让上层只访问他们需要的数据,并且您隐藏了那些层不需要知道的任何东西
- 一个常见的需求是跟踪数据是由谁更改的以及何时更改的,可能是为了审计目的或者为了理解客户行为。您收到的跟踪数据与该类的主要用途是分开的,因此您可以决定使用影子属性实现该数据,这些属性可以在实体类外部获取
- 当您设置关系时,您不需要在实体类中定义外键属性,EF Core 必须添加这些属性才能使关系正常工作,它通过影子属性实现这一点
7.14.1 Configuring shadow properties
modelBuilder.Entity<MyEntityClass>()
.Property<DateTime>("UpdatedOn"); // entity中不存在的属性
7.14.2 Accessing shadow properties
需要通过EF Core 直接访问,而且这个entity必须是 tracked 的
context.Entry(entity).Property("UpdatedOn").CurrentValue
// or
context.MyEntities
.OrderBy(b => EF.Property<DateTime>(b, "UpdatedOn"))
.ToList();
7.15 Backing fields: Controlling access to data in an entity class
可以将私有字段映射到数据库。这个特性称为 Backing fields ,它使您能够更好地控制软件读取或设置数据库数据的方式
对于 shadow 属性,数据隐藏在 EF Core 的数据中,但是 backing fields 隐藏在实体类中,所以实体类更容易访问类中的 backing field
- Hiding sensitive data -- 将一个人的出生日期隐藏在一个私人领域,并将他们的年龄提供给软件的其他部分
- Catching changes -- 通过将数据存储在私有字段中并在 setter 中添加代码来检测属性的更新,从而检测属性的更新
- Creating Domain-Driven Design (DDD) entity classes -- 创建 DDD 实体类,其中所有实体类的属性都需要是只读的。Backing fields 允许您锁定导航收集属性
7.15.1 Creating a simple backing field accessed by a read/write property
public class MyClass
{
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set { _myProperty = value; }
}
}
EF Core 的按约定配置将找到 backing field 的类型并将其配置为一个 backing field ,默认情况下,EF Core 将读/写数据库数据到这个私有字段
7.15.2 Creating a read-only column
创建只读列是最明显的用途,尽管它也可以通过私有设置属性实现(参见7.3.2节)。如果你在数据库中有一个列需要阅读,但又不想让软件写,那么 backing field 就是一个很好的解决方案。在这种情况下,您可以创建一个私有字段并使用一个公共属性(仅带有 getter)来检索值
public class MyClass
{
private string _readOnlyCol;
public string ReadOnlyCol => _readOnlyCol;
}
7.15.3 Concealing a person’s date of birth: Hiding data inside a class
7.15.4 Configuring backing fields
看过 backing fields 的实际应用之后,你可以通过 Fluent API 来按照惯例配置它们,现在通过数据注释在 EF Core 5中配置它们。按约定的方法工作得很好,但依赖于类具有一个按类型和变数命名原则匹配字段的属性。如果一个字段不匹配属性名/类型或者没有匹配属性,比如在 _ dateOfBirth 示例中,你需要使用数据注释或者使用 Fluent API 来配置你的 backing fields。以下各节描述各种配置方法
CONFIGURING BACKING FIELDS BY CONVENTION
- _< property name > (for example, _MyProperty)
- _< camel-cased property name > (for example, _myProperty)
- m_< property name >(for example, m_MyProperty)
- m_< camel-cased property name > (for example, m_myProperty)
CONFIGURING BACKING FIELDS VIA DATA ANNOTATIONS
private string _fieldName;
[BackingField(nameof(_fieldName))]
public string PropertyName
{
get { return _fieldName; }
}
public void SetPropertyNameValue(string someString)
{
_fieldName = someString;
}
CONFIGURING BACKING FIELDS VIA THE FLUENT API
- Setting the name of the backing field
modelBuilder.Entity<Person>()
.Property(b => b.MyProperty)
.HasField("_differentName");
- Supplying only the field name
modelBuilder.Entity<Person>()
.Property("_dateOfBirth")
.HasColumnName("DateOfBirth");
如果没有找到属性 getter 或 setter,字段仍将使用其名称映射到该列,在本例中该名称为 _ dateOfBirth,但这很可能不是您想要的列名称。因此,添加 HasColumnNameFluentAPI 方法以获得更好的列名。缺点是您仍然需要通过字段名称(在本例中为 _ dateOfBirth)来引用查询中的数据,这不太友好或明显。
ADVANCED: CONFIGURING HOW DATA IS READ/WRITTEN TO THE BACKING FIELD
自从 EF Core 3发布以来,backing fields 的默认数据库访问模式是 EF Core 对字段进行读写操作。这种模式几乎在所有情况下都可以工作,但是如果希望更改数据库访问模式,则可以通过 Fluent API UsePropertyAccessMode 方法进行更改。下面的代码片段告诉 EF Core 尝试使用该属性进行读/写操作,但是如果该属性缺少 setter,EF Core 将填充数据库读操作中的字段
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>()
.Property(b => b.MyProperty)
.HasField("_differentName")
.UsePropertyAccessMode(PropertyAccessMode.PreferProperty);
…
}
7.16 Recommendations for using EF Core’s configuration
您有如此多的方法来配置 EF Core,其中一些是相互重复的,以至于您应该为配置的每个部分使用三种方法中的哪一种并不总是显而易见的。下面是针对 EF 核心配置的每一部分提出的建议方法:
- 首先尽可能使用 By Convention 方法,因为它既快速又简单
- 使用数据注释方法中的验证属性ーー MaxLength , Required 等ーー,因为它们对于验证非常有用
- 对于其他所有内容,请使用 FluentAPI 方法,因为它具有最全面的命令集。但是,请考虑编写代码来自动化常见设置,例如将 DateTime“ UTC fix”应用于名称以“ UTC”结尾的所有 DateTime 属性
7.16.4 Automate adding Fluent API commands by class/property signatures
Fluent API 命令的一个有用特性允许您编写代码来根据类/属性类型、名称等来查找和配置某些配置。在实际的应用程序中,可能有数百个 DateTime 属性需要使用 UTC 修复程序。与其手工为每个属性添加配置,不如找到需要 UTC 修复的每个属性并自动应用它,这样不是更好吗?
自动查找/添加配置依赖于一种名为 IMutableModel 的类型,您可以在 OnModelCreate 方法中访问该类型。这种类型允许您访问由 EF Core 映射到数据库的所有类,并且每个 IMutableEntityType 允许您访问属性。大多数配置选项都可以通过这两个接口中的方法应用,但是有些配置选项(例如 Query Filters)需要做一些更多的工作
还能编写更多代码配置
foreach (var entityType in modelBuilder.Model.GetEntityTypes())
{
foreach (var entityProperty in entityType.GetProperties())
{
if (entityProperty.ClrType == typeof(DateTime)
&& entityProperty.Name.EndsWith("Utc"))
{
entityProperty.SetValueConverter(utcConverter);
}
if (entityProperty.ClrType == typeof(decimal)
&& entityProperty.Name.Contains("Price"))
{
entityProperty.SetPrecision(9);
entityProperty.SetScale(2);
}
if (entityProperty.ClrType == typeof(string)
&& entityProperty.Name.EndsWith("Url"))
{
entityProperty.SetIsUnicode(false);
}
}
}
但是,一些 Fluent API 配置需要特定于类的代码。例如,查询过滤器需要访问实体类的查询。对于这种情况,需要向要向其添加查询筛选器的实体类添加一个接口,并动态创建正确的筛选器查询
作为一个示例,您将构建允许自动添加 SoftDelete Query Filter 和 UserId Query Filter 的代码。在这两个查询过滤器中,UserId 更为复杂,因为它需要获取当前的 UserId,该 UserId 在 Book App 的 DbContext 的每个实例上都会更改。可以通过几种方式来实现这一点,但是您决定向查询提供 DbContext 的当前实例
对于 Book App 来说,所有这些自动化操作都有些过头了,但是在更大的应用程序中,它可以为您节省大量的时间; 更重要的是,它确保您已经正确地设置了所有内容。为了结束本节,如果您打算使用这种方法,这里有一些建议和限制,您应该了解这些建议和限制
- 如果在手工编码配置之前运行自动 Fluent API 代码,则手工编码配置将覆盖任何自动 Fluent API 设置。但是请注意,如果有一个实体类仅通过手动编写的 Fluent API 注册,那么自动 Fluent API 代码将不会看到该实体类。
- 配置命令每次都必须应用相同的配置,因为 EF Core 只在第一次使用时配置应用程序的 DbContext 一次,然后在缓存版本中工作。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?