Entity Framework Core 2.0 的新特性

  • Jonathan Allen
  • Rays

2017 年 8 月 24 日

话题:.NET语言 & 开发

虽然EF Core 2.0 存在大量槽点,但是它也给出了不少亮点。在本文中,我们将介绍这次发布版的部分亮点。

数据库表切分(Table Splitting)

ORM 常被吐槽是总是对所请求数据做低效处理。默认情况下,大多数 ORM 执行采用了“SELECT *”的查询语言,这样的语句会请求数据表的全部列,即便应用只是需要其中的小部分子集。

为解决该问题,EF 采用的传统方法是在查询中加入 SELECT 语句,将生成的 SQL 语句限制于查询所需要的列。但是这种方法在实现上或是需要使用第三方的映射库(例如AutoMapper),或是需要显式地列出每个所需的列到投影对象的映射关系。鉴于后一种方法的实现是相当的繁琐,并易于产生错误,在开发周期紧张的情况下开发人员通常跳过该步骤。这会导致数据库不能使用覆盖索引(Covering Index),进而导致一些查询的性能非常差。

另一个问题是,被投影的对象无法参与 CRUD 操作。这些对象或是需要映射回实体,或是需要在原始 SQL 调用中使用。

使用“数据表切分”(Table Splitting)是在 EF Core 中创建更具针对性查询的更好方法。我们可以将多个类映射到同一个数据库表上。要在 EF Core 中使用该方法,必须在所有的共享数据库表的实体类型之间配置一个“区别性关系”(在此关系上,外键属性构成了主键)。

全局过滤器(Global Filters)

我们已经知道,在 EF 中是无法可靠地创建全局过滤器。现在这一特性缺口已被 EF Core 填补。

全局过滤器允许开发人员对访问特定数据库表的所有查询额外添加一模一样的过滤器。它主要用于软删除(soft-delete)场景,即用户并不想返回那些被标记为已删除但是尚未从数据库中做物理移除的数据行。全局过滤器并非一个新概念,NHibernateTortuga Chain及其它一些 ORM 都使用了这一概念。

在 EFCore 中,全局过滤器是使用 OnModelCreating 事件中的如下代码实现的:

modelBuilder.Entity().HasQueryFilter(p => !p.IsDeleted);

该语法的确引发了一些担忧,因为它需要对每个和所有支持 IsDeleted 标识的数据库表做重复性的操作,当存在大量的数据库表时易于出错。但是相比于让开发人员努力去记住在每次查询中做检查,全局过滤器依然作用明显。

全局过滤器可以访问定义在 DbContext 对象中的域。这意味着,开发人员可以将其用于一些更高级的场景中,例如多租户环境。但是在上下文池(Context Pool)中使用它时,不能掉以轻心(参见下文)。

在文档中给出了全局过滤器的两个局限性:

  • 它不允许使用导航引用(Navigation References)。但是有可能会根据反馈的情况而添加对该特性的支持。
  • 它只能定义在一个层次结构的根实体类型(Entity Type)上。

上下文池(Context Pools)

虽然创建 DbContext 对象的代价要显著地低于创建数据库连接,但是前者依然会导致性能下降。一个理想的解决方案是让 DbContext 是线程安全的,但是 EF 的设计并不支持。

为了对 ASP.NET Core 应用解决该问题,现在 EF Core 提供了 DbContext Pooling 特性。该特性是基于 ASP.NET Core 的依赖注入(Dependency Injection)框架实现的。

文档中给出了一个警告,即对于多租户使用全局过滤器的应用场景,上下文池并不兼容。

如果开发人员在派生的 DbContext 类中维护自己的状态(例如私有域),并且该 DbContext 类不应在请求间共享,那么应避免使用 DbContext Pooling 特性。EF Core 仅是在添加一个 DbContext 实例到池中之前,重置了它所了解的状态。

通过“reset”方法,或是 DI 框架所调用的事件,就可以轻易地解决这一问题。鉴于此,我们希望该问题会在未来的版本中得到修正。

标量函数(Scalar Functions)

在数据存放之处执行代码,这是数据库服务器的一个关键特性。如果我们能合理地使用该特性,那么相比于在处理前将数据传输给应用的方法,查询性能可以得到显著的提升。

EF Core 2 通过暴露标量函数,添加了对此特性的支持。标量函数是一种定义在 EF 模型中的静态函数,它的函数体为空,并标记了 DbFunction 属性。LINQ 提供者会检测这些标量函数的使用,并在生成的 SQL 语句中使用相应的服务器端函数。

当前 EF Core 仅支持上述方式的标量函数。除非直接使用原始 SQL 语句,目前 EF Core 尚不提供对更为强大的表值函数(Table Valued Functions)的支持。

原始 SQL 中的字符串插值(String interpolation)

如何让字符串插值与 EF Core 一起工作,这是一个很有意思的特性。EF Core 并非是简单地转换为一个 String.Format 调用,而是发出一个参数化 SQL 字符串。这样做对于避免 SQL 注入攻击非常关键。

var city = "Redmond";
context.Customers.FromSql($"SELECT * FROM Customers WHERE City = {city}");
SELECT * FROM Customers WHERE City = @p0

我们也许可以期待,该技术将会被各种 micro ORM 所采纳。

显式编译查询(Explicitly Compiled Queries)

现在,我们可以将查询以委托的形式缓存(即函数指针),通常定义为一个匿名方法。这不仅提高了查询性能,而且是一种可在多处使用同一查询的便利方法。委托将接受一个 DbContext 对象为参数,因此也可用于多语句事务(Multi-Statement Transaction)中。

请注意,在一些情况下,查询是会自动做缓存的。因此,我们不能对性能的增加期待过高。

EF Core 通常会根据查询表达式的哈希表示而自动地编译并缓存查询。尽管如此,该机制通过旁路哈希和缓存查找的计算,使得应用可以通过调用委托去使用已经被编译过的查询,这依然是可以取得一定程度上的性能增加的。

这些新特性并非凭空给出的。在 EF Core 系列文章的第三部分中,我们将介绍 EF Core 2.0 的一些突破性改进。

查看英文原文: New Features in Entity Framework Core 2.0

.NET语言 & 开发