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

Part 2 Entity Framework in depth

《Entity Framework Core in Action》
-- SECOND EDITION

Author: JON P SMITH

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

10 Configuring advanced features and handling concurrency conflicts

10.1 DbFunction: Using user-defined functions (UDFs) with EF Core

SQL 有一个称为 UDF 的特性,它允许您编写将在数据库服务器中运行的 SQL 代码。UDF 非常有用,因为可以将计算从软件移动到数据库中,这样更有效,因为它可以直接访问数据库。UDF 可以返回单个结果(称为标量值函数) ,也可以返回结果中的多个数据(称为表值函数)。EF Core 支持两种类型的 UDF

SQL 用户定义函数(UDF)是一个例程,它接受参数、执行 SQL 操作(例如复杂的计算)并将该操作的结果作为值返回。返回值可以是标量(单个)值或表。UDF 不同于 SQL 存储过程(StoredProc) ,因为 UDF 只能查询数据库,而 StoredProc 可以更改数据库

在 EFCore 中使用 UDF 的步骤如下:

  1. 定义一个具有与 UDF 定义匹配的正确名称、输入参数和输出类型的方法。此方法充当对 UDF 的引用
  2. 如果方法是标量 UDF,则在应用程序的 DbContext 中或(可选)在单独的类中声明该方法
  3. 添加 EFCore 配置命令将静态 UDF 引用方法映射到对数据库中 UDF 代码的调用
  4. 使用某种形式的 SQL 命令手动将 UDF 代码添加到数据库中
  5. 现在可以在查询中使用静态 UDF 引用。EF Core 将把该方法转换为对数据库中的 UDF 代码的调用

10.1.1 Configuring a scalar-valued UDF

标量值 UDF 的配置包括定义一个表示 UDF 的方法,然后在配置时向 EF Core 注册该方法

可以将 UDF 表示定义为静态或非静态方法。非静态定义需要在应用程序的 DBContext 中定义; 静态版本可以放在单独的类中。我倾向于使用静态定义,因为我不想用额外的代码来打乱应用程序的 DBContext 类

UDF 表示方法用于定义数据库中 UDF 的签名: 它永远不会被作为 .NET 方法调用

您可以使用以下任何一种方法向 EFCore 注册您的静态 UDF 表示方法:

  • DbFunction attribute
  • Fluent API

如果将表示 UDF 的方法放在应用程序的 DbContext 中,则可以使用 DbFunction 属性

另一种方法是使用 FluentAPI 将方法注册为 UDF 表示形式。这种方法的优点是可以将该方法放在任何类中,如果有很多 UDF,这样做是有意义的

10.1.2 Configuring a table-valued UDF

它允许您以查询表返回多个值的相同方式返回多个值。与查询普通表的区别在于,表值 UDF 可以使用提供给 UDF 的参数在数据库中执行 SQL 代码

此示例需要定义一个类,该类将接受从表值 UDF 返回的三个值

public class TableFunctionOutput
{
    public string Title { get; set; }
    public int ReviewsCount { get; set; }
    public double? AverageVotes { get; set; }
}

与标量 UDF 不同,表 UDF 只能以一种方式定义ーー在应用程序的 DbContext 中ーー因为它需要访问名为 FromExpression 的 DbContext 类中的一个方法

10.1.3 Adding your UDF code to the database

第一种方法是通过使用 EFCore 的迁移特性来添加 UDF。为此,您可以使用 MigationBuilder.Sql 方法。通过编辑迁移将这些 UDF 添加到数据库中,然后添加代码来创建这两个 UDF

另一种方法是使用 EF Core 的 ExecuteSqlRaw 或 ExecuteSqlInterpolated 方法添加 UDF,这种方法更适用于单元测试,而不适用于使用迁移创建数据库的生产使用,在这种情况下,必须手动添加 UDF

10.1.4 Using a registered UDF in your database queries

这是直接调用静态方法

10.2 Computed column: A dynamically calculated column value

另一个有用的 SQL 端特性是计算列(也称为生成列)。使用计算列的主要原因是将一些计算(例如某些字符串串联)移入数据库以提高性能。计算列的另一个好用处是根据行中的其他列返回一个有用的值


10.3 Setting a default value for a database column

对于 EFCore,还有两种设置默认值的方法。首先,可以使用 HasDefaultValue Fluent API 方法将 EF Core 配置为在数据库中设置默认值。此方法更改用于在数据库中创建表的 SQL 代码,并添加一个 SQL DEFAULT 命令,该命令包含该列的默认值(如果未提供值)。通常,如果通过原始 SQL 命令将行添加到数据库中,这种方法非常有用,因为对于 SQL INSERT 没有提供值的列,原始 SQL 通常依赖于 SQL DEFAULT 命令

第二种方法是创建您自己的代码,如果没有提供任何值,这些代码将为列创建默认值。此方法要求您编写一个继承 ValueGenerator 类的类,该类将计算默认值。然后,必须通过 ConfigureFluentAPI 方法配置属性以使用 ValueGenerator 类。对于某些类型的值,如为用户的图书订单创建唯一的字符串,这种方法非常有用

在探索每种方法之前,让我们定义一些 EF Core 的默认值设置方法的共同之处:

  • 默认值可以应用于属性、 backing fields 和阴影属性。我们将使用通用术语列来涵盖所有三种类型,因为它们最终都应用于数据库中的一列。
  • 默认值(int、 string、 DateTime、 GUID 等)仅应用于标量(非关系)列。
  • 只有当属性包含与其类型相应的 CLR 默认值时,EFCore 才会提供默认值。例如,如果 int 类型的属性的值为0,那么它就是某种形式的默认值的候选值,但是如果该属性的值不是0,那么将使用非零值
  • EFCore 的默认值方法工作在实体实例级别,而不是类级别。在调用 SaveChanges 或(在使用值生成器的情况下)使用 Add 命令添加实体之前,不会应用默认值。

需要说明的是: 默认值只发生在添加到数据库的新行上,而不发生在更新上

可以通过三种方式配置 EFCore 以添加默认值:

  • HasDefaultValue
  • HasDefaultValueSql
  • HasValueGenerator

10.3.1 Using the HasDefaultValue method to add a constant value for a column

modelBuilder.Entity<DefaultTest>()
    .Property("DateOfBirth")
    .HasDefaultValue(new DateTime(2000,1,1));

10.3.2 Using the HasDefaultValueSql method to add an SQL command for a column

更有用的是访问一些返回当前日期/时间的 SQL 系统函数,HasDefaultValueSql 方法允许您这样做

modelBuilder.Entity<DefaultTest>()
    .Property(x => x.CreatedOn)
    .HasDefaultValueSql("getutcdate()");

这个特性很有用。除了访问诸如 getutcdate 之类的系统函数之外,还可以将自己的 SQL UDF 放在默认约束中。可以放置的 SQL 命令有一个限制ーー不能引用默认约束中的另一列

10.3.3 Using the HasValueGenerator method to assign a value generator to a property

如果以下两个语句都为 true,则将要求此类提供默认值:

  • 实体的状态被设置为“Added”; 该实体被视为要添加到数据库中的新实体
  • 该属性尚未设置; 其值为.NET 类型的默认值

如果需要实现 async 版本,可以使用 NextAsync 版本,例如在生成默认版本时使用 async 方法访问数据库。在这种情况下,在向数据库添加实体时需要使用 AddAsync 方法

10.6 Handling simultaneous updates: Concurrency conflicts

10.6.1 Why do concurrency conflicts matter?

有时,通过设计来避免并发冲突,创建应用程序的方式可以避免危险的并发更新。例如,对于我设计的一个电子商务网站,我有一个使用后台任务的订单处理系统,这可能会导致并发冲突。我通过设计订单处理来消除并发更新的可能性,从而避免了这个潜在的问题

  • 我将客户订单信息分割成一个永远不变的订单部分。这一部分包含了诸如订购了什么以及应该发送到哪里等数据。创建该订单后,该订单从未更改或删除
  • 对于订单的变化部分,即订单在系统中移动时的订单状态,我创建了一个单独的表,在其中添加每个新订单状态,包括日期和时间。(这种方法称为事件源。)然后通过按日期/时间顺序排序并选择最新的日期和时间来获得最新的订单状态。当然,如果在读取状态之后添加了另一个状态,那么这个结果将过时,但是并发处理将检测到这个添加。

这种设计方法意味着我从不更新或删除任何订单数据,因此不会发生并发冲突。它确实使得处理客户更改到订单变得有点复杂,但订单不会受到并发冲突问题的影响。

但是当并发冲突成为问题时,你不能围绕它们进行设计,EF Core 提供了两种检测并发更新的方法,当检测到更新时,一种获取所有相关数据的方法,这样你就可以实现代码来解决这个问题。

10.6.2 EF Core’s concurrency conflict–handling features

EF Core 的并发冲突处理特性可以通过两种方式检测并发更新,通过向实体类添加以下内容之一来激活:

  • 将实体类中的特定属性/列标记为一个并发标记,以检查并发冲突
  • 时间戳(也称为 rowversion) ,它将整个实体类/行标记,以检查并发冲突

在这两种情况下,当调用 SaveChanges 时,EF Core 产生数据库服务器代码来检查包含并发令牌或时间戳的任何实体的更新。如果该代码检测到并发令牌或时间戳在读取实体后发生了更改,则抛出 DbUpdateConcurrencyException 异常。此时,您可以使用 EF Core 的特性来检查数据的不同版本,并应用自定义代码来决定哪个并发更新获胜。接下来,您将学习如何设置这两种方法ーー并发令牌和时间戳ーー以及 EF Core 如何检测更改

DETECTING A CONCURRENT CHANGE VIA CONCURRENCY TOKEN

并发令牌方法允许您将一个或多个属性配置为并发令牌。这种方法告诉 EF Core 检查当前数据库值是否与作为发送到数据库的 SQL UPDATE 命令的一部分加载跟踪实体时发现的值相同。这样,如果加载的值和当前数据库值不同,更新将失败

public class ConcurrencyBook
{
    public int ConcurrencyBookId { get; set; }
    public string Title { get; set; }

    [ConcurrencyCheck]
    public DateTime PublishedOn { get; set; }

    public ConcurrencyAuthor Author { get; set; }
}

或者,您可以通过 FluentAPI 定义一个并发令牌

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ConcurrencyBook>()
        .Property(p => p.PublishedOn)
        .IsConcurrencyToken();
}

使用并发令牌的好处是它可以在任何数据库上工作,因为它使用基本命令

DETECTING A CONCURRENT CHANGE VIA TIMESTAMP

检查并发冲突的第二种方法是使用 EF Core 所称的时间戳。时间戳的工作方式与并发令牌不同,因为它使用数据库服务器提供的唯一值,该值在插入或更新行时发生更改。保护整个实体(而不是特定的属性或列)不受并发更改的影响

时间戳数据库类型是特定于数据库类型的: SQL Server 的并发类型是 ROWVERION,它映射到 .NET 中的 byte [] ; PostgreSQL 有一个名为 xmin 的列,它是一个无符号的32位数字; Cosmos DB 有一个名为 _etag 的 JSON 属性,它是一个包含唯一值的字符串。EF Core 可以通过适当的数据库提供程序使用这些类型中的任何一种

public class ConcurrencyAuthor
{
    public int ConcurrencyAuthorId { get; set; }
    public string Name { get; set; }
    [Timestamp]
    public byte[] ChangeCheck { get; set; }
}
modelBuilder.Entity<ConcurrencyAuthor>()
    .Property(p => p.ChangeCheck)
    .IsRowVersion();

在这种情况下,ChangeCheck 属性有一个 Timestamp Data Annote,它告诉 EF Core 将其标记为一个特殊列,数据库将用唯一值更新该列。对于 SQLServer,数据库提供程序将该列设置为 SQLServer 行版本; 其他数据库有不同的实现 TimeStamp 列的方法

10.6.3 Handling a DbUpdateConcurrencyException


需要更改的主要部分是以注释//TODO 开始的部分。您应该将处理并发更新的代码放在那里。放在那里的内容取决于应用程序中的业务规则。重点放在数据的三个部分: 原始值、其他用户的值以及您希望 PublishedOn 值是什么

posted @   huang1993  阅读(164)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示