Entity Framework Core 6.0预览版5:编译的模型背景和优劣介绍

181 阅读8分钟

今天,Entity Framework Core团队宣布了EF Core 6.0的第五个预览版本。这个版本包括编译模型的第一次迭代。如果你的应用程序的启动时间很重要,而且你的EF Core模型包含成百上千的实体、属性和关系,那么这是你不想忽视的一个版本。

TL;DR;

  • 编译的模型极大地减少了你的应用程序的启动时间。
  • 这些模型是生成的(类似于迁移的方式),所以每当你的模型发生变化时,它们应该被刷新。
  • 一些功能目前不被编译模型所支持,所以当你尝试这些功能时要注意其局限性。

背景

你觉得10倍的性能如何?我们的团队创建了一个样本项目,其中有一个DbContext ,包含449个实体类型,6390个属性和720个关系。我写了一个控制台应用程序,循环了几次,创建了一个新的DbContext 的实例,并加载了一组实体,没有过滤器或排序。在我的笔记本电脑上,第一次运行的启动时间一直在2秒左右,随后的缓存实例大约为1.5秒。这是一次运行的输出:

$ dotnet run -c Release
Model has:
  449 entity types
  6390 properties
  720 relationships
Instantiating context...
It took 00:00:02.1603163.
Instantiating context...
It took 00:00:01.6268628.
Instantiating context...
It took 00:00:01.7144346.
Instantiating context...
It took 00:00:01.6090380.
Instantiating context...
It took 00:00:01.7049987.

在测试了基线应用程序后,我使用新的EF Core工具的命令行界面(CLI)功能来优化DbContext

dotnet ef dbcontext optimize -output-dir MyCompiledModels --namespace MyCompiledModels

该工具给我的指示是在我的DbContext 配置中添加一行代码:

options.UseModel(MyCompiledModels.BlogsContextModel.Instance);

我进行了更新并重新运行了代码,得到了10倍的性能提升,初始模型需要257ms完成。缓存模型将额外的调用减少到只有10ms

$ dotnet run -c Release
Model has:
  449 entity types
  6390 properties
  720 relationships
Instantiating context...
It took 00:00:00.2573627.
Instantiating context...
It took 00:00:00.0132345.
Instantiating context...
It took 00:00:00.0119556.
Instantiating context...
It took 00:00:00.0101717.
Instantiating context...
It took 00:00:00.0139057.

对查询管道的窥视

从你的应用程序到返回你的应用程序处理的第一个查询的第一个结果,EF Core执行了相当多的工作。让我们来分解以下两个语句,看看 "幕后 "发生了什么:

using var myContext = new MyContext();
var results = myContext.MyWidgets.ToList();

DbContext实例化

第一步是创建一个上下文的实例。第一次创建DbContext ,EF Core将创建和编译委托,以设置你通过使用DbSet<Entity> 暴露的表属性。这只是创建了设置属性的委托,所以你可以立即查询它们:

性能提示: 你可以通过使用另一种方法来避免DbSet 初始化的开销,例如context.Set<Entity>() API调用。

DbContext(懒)初始化

在创建了DbContext ,EF Core就会 "进入睡眠状态",直到你使用它。当你第一次通过访问它的一个API来使用一个上下文的时候(比如导航一个实体并返回结果),这个上下文就会被初始化。这将运行OnConfiguring 方法来建立适当的提供者和数据库连接,以及其他设置。例如,这是通过调用选项生成器上新的LogTo 扩展来使用简单日志功能的完美地方。

服务提供者

EF Core使用一个基于服务的架构,并且有一个内部的依赖注入框架。这个提供者是内部构建的,但被设计为与外部DI解决方案一起工作,如ASP.NET Core中的服务提供者

性能提示: 到目前为止所描述的大部分开销可以通过使用上下文池来缓解。这使得一个已经被初始化的可重复使用的上下文实例池成为可能。

模型构建

为了理解领域对象(C#类)与数据库中的表和关系的关系,EF Core建立了一个内部模型,代表了它在你的DbContext 中发现的所有类型、属性、约束和关系。这是一个元数据模型,包括对OnModelCreating 的调用,可以被重写以提供模型的流畅配置。

查询的编译

开发人员使用EF Core的一个主要原因是它能够将语言集成查询(LINQ)解析为数据库方言。这是一个高级阶段,因为它涉及到遍历一个潜在的复杂表达式树并将其翻译成SQL。一些琐碎的东西,比如一个投影:

var projection = myQuery.Select(obj => new { id = obj.EntityId, name = obj.Identifier });

看上去很容易翻译:

SELECT EntityId, Identifier FROM ...

但是更复杂的东西呢,比如这个:

var pairs = (from a1 in context.Attendees
                from a2 in context.Attendees
                where a1.Id != a2.Id
                select new
                {
                    a1 = a1.Id,
                    a1LastName = a1.LastName,
                    a1FirstName = a2.FirstName,
                    a2 = a2.Id,
                    a2LastName = a2.LastName,
                    a2FirstName = a2.FirstName,
                    sessionCount = 
                    a1.Sessions.Select(s => s.Id)
                    .Intersect(a2.Sessions.Select(s => s.Id)).Count()
                }).OrderByDescending(shared => shared.sessionCount)
            .Take(5);

这最终会被解析成本地的SQL,交叉和所有的。当EF Core第一次遇到一个查询的时候,它会解析这个查询以确定哪些部分是动态的。然后,它编译查询的静态部分,并对动态部分进行参数化,通过使用SQL模板来加速翻译成SQL。

运行查询

终于来了!现在可以运行查询了。为了避免每次执行这些步骤的开销,EF Core缓存了DbSet 属性、内部服务提供者、构建的模型和编译后的查询的委托。这导致了在查询第一次成功运行后,性能大大提升。

你可以用下图来说明这些步骤(注意缓存框有醒目的标记,表明它们在我们的基准测试中被禁用):

EF Core initialization steps, Announcing Entity Framework Core 6.0 Preview 5

虽然大部分的管道已经被简化,但模型编译是我们知道可以改进的一个领域。

关于源码生成器的说明。 团队选择的方法是提供一个生成源代码文件的命令,然后你可以将其纳入你的项目,以建立编译后的模型。经常有人问我们为什么不选择源码生成器。答案是,源码生成器在Visual Studio过程中作为用户代码运行。EF Core必须构建和运行上下文以获得模型的信息。如果在这个过程中抛出一个异常,这有可能迫使Visual Studio挂起或崩溃。

和大多数技术一样,编译的模型也是有取舍的。让我们来看看其优点和缺点。

优点和缺点

优点应该很明显。随着你的模型越来越大,你的启动时间仍然很快。这里是基于模型大小的编译和非编译模型的启动时间的比较。

Startup time by model size, Announcing Entity Framework Core 6.0 Preview 5

这里有一些需要考虑的缺点。

提示: 如果支持这些功能中的任何一个对你的成功至关重要,请找到这个问题并加注,或添加你的评论和想法,或提交一个新问题让我们知道。

现在你已经了解了背景。你如何开始工作?

综上所述

要想今天就开始使用编译后的模型,获得性能上的好处,并有机会在我们发布最终的EF Core 6.0版本之前向我们提供反馈,请从抓取最新的预览版开始(说明见下文),并安装最新的EF Core CLI。新的工具命令看起来像这样(所有参数都是可选的):

dotnet ef dbcontext optimize -c MyContext -o MyFolder -n My.Namespace 

在NuGet包管理器的控制台里面,你可以使用这个:

Optimize-DbContext -Context MyContext -OutputDir MyFolder -Namespace My.Namespace

该工具会指示你在你的选项配置中添加这样一行字:

opts.UseModel(My.Namespace.MyContextModel.Instance);

我们希望你能从这个新功能中受益,并能向我们提供早期反馈。查看EF Core 6.0计划。除了其他工作外,该团队还优先考虑了一些Azure Cosmos DB提供商的功能。请为那些对你来说很重要的功能加注,并分享你可能有的任何反馈!预览5版本中的其他功能将在EF Core 6.0 What's New中发布。

如何获得EF Core 6.0预览版

EF Core是以一套NuGet包的形式独家发布的。例如,要将SQL Server提供者添加到你的项目中,你可以使用dotnet工具使用以下命令:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer --version 6.0.0-preview.5.21301.9

下表链接到EF Core包的预览5版本,并描述了它们的用途。

用途
Microsoft.EntityFrameworkCore独立于特定数据库提供者的主要EF Core包。
Microsoft.EntityFrameworkCore.SqlServerMicrosoft SQL Server和SQL Azure的数据库提供商。
Microsoft.EntityFrameworkCore.SqlServer.NetTopologySuiteSQL Server对空间类型的支持
Microsoft.EntityFrameworkCore.SqliteSQLite的数据库提供者,包括数据库引擎的本地二进制文件
Microsoft.EntityFrameworkCore.Sqlite.Core用于SQLite的数据库提供者,没有打包的本地二进制文件
Microsoft.EntityFrameworkCore.Sqlite.NetTopologySuiteSQLite对空间类型的支持
微软.EntityFrameworkCore.Cosmos用于Azure Cosmos DB的数据库提供商
Microsoft.EntityFrameworkCore.InMemory内存数据库提供者
微软.EntityFrameworkCore.Tools用于Visual Studio Package Manager Console的EF Core PowerShell命令;使用它可以将脚手架迁移等工具与Visual Studio集成起来。
Microsoft.EntityFrameworkCore.Design用于EF Core工具的共享设计时组件
Microsoft.EntityFrameworkCore.Proxies懒惰的加载和变化跟踪的代理
Microsoft.EntityFrameworkCore.Abstractions解耦的EF Core抽象;使用它来实现EF Core定义的扩展数据注释等功能。
Microsoft.EntityFrameworkCore.Relational用于关系型数据库提供者的共享的EF Core组件
Microsoft.EntityFrameworkCore.Analyzers用于EF Core的C#分析器

我们还发布了用于ADO.NETMicrosoft.Data.Sqlite.Core提供者的6.0预览5版。