本文介绍初始化和配置 DbContext 实例的基本模式。
DbContext 生存期
DbContext
的生存期从创建实例时开始,并在释放实例时结束。 DbContext
实例旨在用于单个工作单元。 这意味着 DbContext
实例的生存期通常很短。
提示
引用上述链接中 Martin Fowler 的话,“工作单元将持续跟踪在可能影响数据库的业务事务中执行的所有操作。 当你完成操作后,它将找出更改数据库作为工作结果时需要执行的所有操作。”
使用 Entity Framework Core (EF Core) 时的典型工作单元包括:
- 创建
DbContext
实例 - 根据上下文跟踪实体实例。 实体将在以下情况下被跟踪
- 正在从查询返回
- 正在添加或附加到上下文
- 根据需要对所跟踪的实体进行更改以实现业务规则
- 调用 SaveChanges 或 SaveChangesAsync。 EF Core 检测所做的更改,并将这些更改写入数据库。
- 释放
DbContext
实例
重要
- 使用后释放 DbContext 非常重要。 这可确保释放所有非托管资源,并注销任何事件或其他挂钩,以防止在实例保持引用时出现内存泄漏。
- DbContext 不是线程安全的。 不要在线程之间共享上下文。 请确保在继续使用上下文实例之前,等待所有异步调用。
- EF Core 代码引发的 InvalidOperationException 可以使上下文进入不可恢复的状态。 此类异常指示程序错误,并且不旨在从其中恢复。
ASP.NET Core 依赖关系注入中的 DbContext
在许多 Web 应用程序中,每个 HTTP 请求都对应于单个工作单元。 这使得上下文生存期与请求的生存期相关,成为 Web 应用程序的一个良好默认值。
使用依赖关系注入配置 ASP.NET Core 应用程序。 可以使用 Startup.cs
的 ConfigureServices
方法中的 AddDbContext 将 EF Core 添加到此配置。 例如:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddDbContext<ApplicationDbContext>(
options => options.UseSqlServer("name=ConnectionStrings:DefaultConnection"));
}
此示例将名为 ApplicationDbContext
的 DbContext
子类注册为 ASP.NET Core 应用程序服务提供程序(也称为依赖关系注入容器)中的作用域服务。 上下文配置为使用 SQL Server 数据库提供程序,并将从 ASP.NET Core 配置读取连接字符串。 在 ConfigureServices
中的何处调用 AddDbContext
通常不重要。
ApplicationDbContext
类必须公开具有 DbContextOptions<ApplicationDbContext>
参数的公共构造函数。 这是将 AddDbContext
的上下文配置传递到 DbContext
的方式。 例如:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
然后,ApplicationDbContext
可以通过构造函数注入在 ASP.NET Core 控制器或其他服务中使用。 例如:
public class MyController
{
private readonly ApplicationDbContext _context;
public MyController(ApplicationDbContext context)
{
_context = context;
}
}
最终结果是为每个请求创建一个 ApplicationDbContext
实例,并传递给控制器,以在请求结束后释放前执行工作单元。
使用“new”的简单的 DbContext 初始化
可以按照常规的 .NET 方式构造 DbContext
实例,例如,使用 C# 中的 new
。 可以通过重写 OnConfiguring
方法或通过将选项传递给构造函数来执行配置。 例如:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
}
}
通过此模式,还可以轻松地通过 DbContext
构造函数传递配置(如连接字符串)。 例如:
public class ApplicationDbContext : DbContext
{
private readonly string _connectionString;
public ApplicationDbContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(_connectionString);
}
}
或者,可以使用 DbContextOptionsBuilder
创建 DbContextOptions
对象,然后将该对象传递到 DbContext
构造函数。 这使得为依赖关系注入配置的 DbContext
也能显式构造。 例如,使用上述为 ASP.NET Core 的 Web 应用定义的 ApplicationDbContext
时:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
可以创建 DbContextOptions
,并可以显式调用构造函数:
var contextOptions = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test")
.Options;
using var context = new ApplicationDbContext(contextOptions);
使用 DbContext 工厂(例如对于 Blazor)
某些应用程序类型(例如 ASP.NET Core Blazor)使用依赖关系注入,但不创建与所需的 DbContext
生存期一致的服务作用域。 即使存在这样的对齐方式,应用程序也可能需要在此作用域内执行多个工作单元。 例如,单个 HTTP 请求中的多个工作单元。
在这些情况下,可以使用 AddDbContextFactory 来注册工厂以创建 DbContext
实例。 例如:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContextFactory<ApplicationDbContext>(
options =>
options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test"));
}
ApplicationDbContext
类必须公开具有 DbContextOptions<ApplicationDbContext>
参数的公共构造函数。 此模式与上面传统 ASP.NET Core 部分中使用的模式相同。
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
}
然后,可以通过构造函数注入在其他服务中使用 DbContextFactory
工厂。 例如:
private readonly IDbContextFactory<ApplicationDbContext> _contextFactory;
public MyController(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
然后,可以使用注入的工厂在服务代码中构造 DbContext 实例。 例如:
public void DoSomething()
{
using (var context = _contextFactory.CreateDbContext())
{
// ...
}
}
请注意,以这种方式创建的 DbContext
实例并非由应用程序的服务提供程序进行管理,因此必须由应用程序释放。
DbContextOptions
所有 DbContext
配置的起始点都是 DbContextOptionsBuilder。 可以通过三种方式获取此生成器:
- 在
AddDbContext
和相关方法中 - 在
OnConfiguring
中 - 使用
new
显式构造
上述各节显示了其中每个示例。 无论生成器来自何处,都可以应用相同的配置。 此外,无论如何构造上下文,都将始终调用 OnConfiguring
。 这意味着即使使用 AddDbContext
,OnConfiguring
也可用于执行其他配置。
配置数据库提供程序
每个 DbContext
实例都必须配置为使用一个且仅一个数据库提供程序。 (DbContext
子类型的不同实例可用于不同的数据库提供程序,但一个实例只能使用一个。)一个数据库提供程序要使用一个特定的 Use*
调用进行配置。 例如,若要使用 SQL Server 数据库提供程序:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
}
}
这些 Use*
方法是由数据库提供程序实现的扩展方法。 这意味着必须先安装数据库提供程序 NuGet 包,然后才能使用扩展方法。
提示
EF Core 数据库提供程序广泛使用扩展方法。 如果编译器指示找不到方法,请确保已安装提供程序的 NuGet 包,并且你在代码中已有 using Microsoft.EntityFrameworkCore;
。
下表包含常见数据库提供程序的示例。
数据库系统 | 配置示例 | NuGet 程序包 |
---|---|---|
SQL Server 或 Azure SQL | .UseSqlServer(connectionString) | Microsoft.EntityFrameworkCore.SqlServer |
Azure Cosmos DB | .UseCosmos(connectionString, databaseName) | Microsoft.EntityFrameworkCore.Cosmos |
SQLite | .UseSqlite(connectionString) | Microsoft.EntityFrameworkCore.Sqlite |
EF Core 内存中数据库 | .UseInMemoryDatabase(databaseName) | Microsoft.EntityFrameworkCore.InMemory |
PostgreSQL* | .UseNpgsql(connectionString) | Npgsql.EntityFrameworkCore.PostgreSQL |
MySQL/MariaDB* | .UseMySql(connectionString) | Pomelo.EntityFrameworkCore.MySql |
Oracle* | .UseOracle(connectionString) | Oracle.EntityFrameworkCore |
*这些数据库提供程序不由 Microsoft 提供。
警告
EF Core 内存中数据库不是为生产用途设计的。 此外,它可能不是测试的最佳选择。
特定于数据库提供程序的可选配置是在其他特定于提供程序的生成器中执行的。 例如,在连接到 Azure SQL 时,使用 EnableRetryOnFailure 为连接复原配置重试:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseSqlServer(
@"Server=(localdb)\mssqllocaldb;Database=Test",
providerOptions => { providerOptions.EnableRetryOnFailure(); });
}
}
提示
同一数据库提供程序用于 SQL Server 和 Azure SQL。 但是,建议在连接到 SQL Azure 时使用连接复原。
其他 DbContext 配置
其他 DbContext
配置可以链接到 Use*
调用之前或之后(这不会有任何差别)。 例如,若要启用敏感数据日志记录:
public class ApplicationDbContext : DbContext
{
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.EnableSensitiveDataLogging()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Test");
}
}
下表包含 DbContextOptionsBuilder
调用的常见方法的示例。
DbContextOptionsBuilder 方法 | 作用 | 了解更多 |
---|---|---|
UseQueryTrackingBehavior | 设置查询的默认跟踪行为 | 查询跟踪行为 |
LogTo | 获取 EF Core 日志的一种简单方法(EF Core 5.0 及更高版本) | 日志记录、事件和诊断 |
UseLoggerFactory | 注册 Microsoft.Extensions.Logging 工厂 | 日志记录、事件和诊断 |
EnableSensitiveDataLogging | 在异常和日志记录中包括应用程序数据 | 日志记录、事件和诊断 |
EnableDetailedErrors | 更详细的查询错误(以性能为代价) | 日志记录、事件和诊断 |
ConfigureWarnings | 忽略或引发警告和其他事件 | 日志记录、事件和诊断 |
AddInterceptors | 注册 EF Core 侦听器 | 日志记录、事件和诊断 |
UseLazyLoadingProxies | 使用动态代理进行延迟加载 | 延迟加载 |
UseChangeTrackingProxies | 使用动态代理进行更改跟踪 | 即将推出... |
备注
UseLazyLoadingProxies 和 UseChangeTrackingProxies 是 Microsoft.EntityFrameworkCore.Proxies NuGet 包中的扩展方法。 建议的方式是使用这种类型的“.UseSomething()”调用来配置和/或使用其他包中包含的 EF Core 扩展。
DbContextOptions
与 DbContextOptions<TContext>
大多数接受 DbContextOptions
的 DbContext
子类应使用 泛型DbContextOptions<TContext>
变体。 例如:
public sealed class SealedApplicationDbContext : DbContext
{
public SealedApplicationDbContext(DbContextOptions<SealedApplicationDbContext> contextOptions)
: base(contextOptions)
{
}
}
这可确保从依赖关系注入中解析特定 DbContext
子类型的正确选项,即使注册了多个 DbContext
子类型也是如此。
提示
你的 DbContext 不需要密封,但对于没有被设计为继承的类,密封是最佳做法。
但是,如果 DbContext
子类型本身旨在继承,则它应公开采用非泛型 DbContextOptions
的受保护构造函数。 例如:
public abstract class ApplicationDbContextBase : DbContext
{
protected ApplicationDbContextBase(DbContextOptions contextOptions)
: base(contextOptions)
{
}
}
这允许多个具体子类使用其不同的泛型 DbContextOptions<TContext>
实例来调用此基构造函数。 例如:
public sealed class ApplicationDbContext1 : ApplicationDbContextBase
{
public ApplicationDbContext1(DbContextOptions<ApplicationDbContext1> contextOptions)
: base(contextOptions)
{
}
}
public sealed class ApplicationDbContext2 : ApplicationDbContextBase
{
public ApplicationDbContext2(DbContextOptions<ApplicationDbContext2> contextOptions)
: base(contextOptions)
{
}
}
请注意,这与直接从 DbContext
继承的模式完全相同。 也就是说,出于此原因,DbContext
构造函数本身将接受非泛型 DbContextOptions
。
旨在同时进行实例化和继承的 DbContext
子类应公开构造函数的两种形式。 例如:
public class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> contextOptions)
: base(contextOptions)
{
}
protected ApplicationDbContext(DbContextOptions contextOptions)
: base(contextOptions)
{
}
}
设计时 DbContext 配置
EF Core 设计时工具(如 EF Core迁移)需要能够发现并创建 DbContext
类型的工作实例,以收集有关应用程序的实体类型及其如何映射到数据库架构的详细信息。 只要该工具可以通过与在运行时的配置方式类似的方式轻松地创建 DbContext
,就可以自动执行此过程。
虽然向 DbContext
提供必要配置信息的任何模式都可以在运行时正常运行,但在设计时需要使用 DbContext
的工具只能使用有限数量的模式。 设计时上下文创建中包含更多详细信息。
避免 DbContext 线程处理问题
Entity Framework Core 不支持在同一 DbContext
实例上运行多个并行操作。 这包括异步查询的并行执行以及从多个线程进行的任何显式并发使用。 因此,始终立即 await
异步调用,或对并行执行的操作使用单独的 DbContext
实例。
当 EF Core 检测到尝试同时使用 DbContext
实例的情况时,你将看到 InvalidOperationException
,其中包含类似于以下内容的消息:
在上一个操作完成之前,第二个操作已在此上下文中启动。 这通常是由使用同一个 DbContext 实例的不同线程引起的,但不保证实例成员是线程安全的。
检测不到并发访问时,可能会导致未定义的行为、应用程序崩溃和数据损坏。
一些常见错误可能会无意中导致并发访问同一 DbContext
实例:
异步操作缺陷
使用异步方法,EF Core 可以启动以非阻挡式访问数据库的操作。 但是,如果调用方不等待其中一个方法完成,而是继续对 DbContext
执行其他操作,则 DbContext
的状态可能会(并且很可能会)损坏。
始终立即等待 EF Core 异步方法。
通过依赖关系注入隐式共享 DbContext 实例
默认情况下 AddDbContext
扩展方法使用DbContext
范围内生存期来注册 类型。
这样可以避免在大多数 ASP.NET Core 应用程序中出现并发访问问题,因为在给定时间内只有一个线程在执行每个客户端请求,并且每个请求都有单独的依赖关系注入范围(因此有单独的 DbContext
实例)。 对于 Blazor Server 托管模型,一个逻辑请求用来维护 Blazor 用户线路,因此,如果使用默认注入范围,则每个用户线路只能提供一个范围内的 DbContext 实例。
任何并行显式执行多个线程的代码都应确保 DbContext
实例不会同时访问。
使用依赖关系注入可以通过以下方式实现:将上下文注册为范围内,并为每个线程创建范围(使用 IServiceScopeFactory
),或将 DbContext
注册为暂时性(使用采用 ServiceLifetime
参数的 AddDbContext
的重载)。
DbContext 池
DbContext
通常是一个轻型对象:创建和释放它不涉及数据库操作,而大多数应用程序都可以这样做,而不会对性能产生任何明显的影响。 但是,每个上下文实例确实设置了执行其职责所需的各种内部服务和对象,并且持续执行此操作的开销在高性能方案中可能十分重要。 在这些情况下,EF Core 可以 共用 上下文实例:释放上下文时,EF Core 会重置其状态并将其存储在内部池中;下次请求新实例时,将返回该共用实例,而不是设置新实例。 上下文池允许你在程序启动时只支付一次上下文设置成本,而不是持续支付。
请注意,上下文池与数据库连接池正交,该池在数据库驱动程序的较低级别进行管理。
具有依赖关系注入
使用 EF Core 的 ASP.NET Core 应用中的典型模式涉及通过 AddDbContext 将自定义DbContext类型注册到依赖项注入容器中。 然后,将通过控制器或 Razor Pages 中的构造函数参数获取该类型的实例。
若要启用上下文池,只需替换为AddDbContext
AddDbContextPool:
builder.Services.AddDbContextPool<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
poolSize
池保留的最大实例数的参数AddDbContextPool (在 EF Core 6.0 中默认为 1024,在以前的版本中为 128) 。 一旦超过 poolSize
,就不会缓存新的上下文实例,EF 会回退到按需创建实例的非池行为。
没有依赖关系注入
备注
EF Core 6.0 引入无需依赖关系注入的池。
若要在不进行依赖项注入的情况下使用上下文池,请初始化 PooledDbContextFactory
上下文实例并从中请求上下文实例:
var options = new DbContextOptionsBuilder<PooledBloggingContext>()
.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=Blogging;Trusted_Connection=True")
.Options;
var factory = new PooledDbContextFactory<PooledBloggingContext>(options);
using (var context = factory.CreateDbContext())
{
var allPosts = context.Posts.ToList();
}
PooledDbContextFactory
构造函数的 poolSize
参数设置池保留的最大实例数(在 EF Core 6.0 中默认为 1024,在以前的版本中为 128)。 一旦超过 poolSize
,就不会缓存新的上下文实例,EF 会回退到按需创建实例的非池行为。
基准
下面是用于从同一台计算机本地运行的SQL Server数据库的单个行(且不包含上下文池)获取单行的基准结果。 与往常一样,结果将随着行数、数据库服务器延迟和其他因素而变化。 重要的是,这测试了单线程池的性能,而实际的争用方案可能会有不同的结果,在作出任何决定之前先对平台进行基准测试。 此处提供了源代码,请根据需要将它用作自己的度量的基础。
方法 | NumBlogs | 平均值 | 错误 | 标准偏差 | Gen 0 | Gen 1 | Gen 2 | 已分配 |
---|---|---|---|---|---|---|---|---|
WithoutContextPooling | 1 | 701.6 us | 26.62 us | 78.48 us | 11.7188 | - | - | 50.38 KB |
WithContextPooling | 1 | 350.1 us | 6.80 us | 14.64 us | 0.9766 | - | - | 4.63 KB |
管理共用上下文中的状态
上下文池的工作原理是跨请求重用相同的上下文实例;这意味着它实际上注册为 单一实例,并且同一实例在多个请求 (或 DI 范围) 重复使用。 这意味着,当上下文涉及任何可能在请求之间更改的状态时,必须特别注意。 关键是, OnConfiguring
上下文仅在首次创建实例上下文时调用一次,因此不能用于设置需要改变 (的状态,例如租户 ID) 。
涉及上下文状态的典型方案是多租户 ASP.NET Core应用程序,其中上下文实例具有由查询考虑的租户 ID,。 由于租户 ID 需要随每个 Web 请求更改,因此我们需要执行一些额外的步骤,使其全部适用于上下文池。
假设应用程序注册范围 ITenant
服务,该服务包装租户 ID 和其他任何租户相关信息:
// Below is a minimal tenant resolution strategy, which registers a scoped ITenant service in DI.
// In this sample, we simply accept the tenant ID as a request query, which means that a client can impersonate any
// tenant. In a real application, the tenant ID would be set based on secure authentication data.
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ITenant>(sp =>
{
var tenantIdString = sp.GetRequiredService<IHttpContextAccessor>().HttpContext.Request.Query["TenantId"];
return tenantIdString != StringValues.Empty && int.TryParse(tenantIdString, out var tenantId)
? new Tenant(tenantId)
: null;
});
如上所述,请特别注意从何处获取租户 ID - 这是应用程序安全性的一个重要方面。
创建范围 ITenant
服务后,像往常一样将池上下文工厂注册为单一实例服务:
builder.Services.AddPooledDbContextFactory<WeatherForecastContext>(
o => o.UseSqlServer(builder.Configuration.GetConnectionString("WeatherForecastContext")));
接下来,编写一个自定义上下文工厂,该工厂从我们注册的 Singleton 工厂获取共用上下文,并将租户 ID 注入到它移交的上下文实例中:
public class WeatherForecastScopedFactory : IDbContextFactory<WeatherForecastContext>
{
private const int DefaultTenantId = -1;
private readonly IDbContextFactory<WeatherForecastContext> _pooledFactory;
private readonly int _tenantId;
public WeatherForecastScopedFactory(
IDbContextFactory<WeatherForecastContext> pooledFactory,
ITenant tenant)
{
_pooledFactory = pooledFactory;
_tenantId = tenant?.TenantId ?? DefaultTenantId;
}
public WeatherForecastContext CreateDbContext()
{
var context = _pooledFactory.CreateDbContext();
context.TenantId = _tenantId;
return context;
}
}
创建自定义上下文工厂后,将其注册为作用域服务:
builder.Services.AddScoped<WeatherForecastScopedFactory>();
最后,安排上下文以从作用域工厂注入:
builder.Services.AddScoped(
sp => sp.GetRequiredService<WeatherForecastScopedFactory>().CreateDbContext());
此时,控制器会自动注入具有适当租户 ID 的上下文实例,而无需知道任何相关信息。
此处提供了此示例的完整源代码。
备注
尽管 EF Core 负责重置内部状态 DbContext
及其相关服务,但它通常不会在 EF 外部的基础数据库驱动程序中重置状态。 例如,如果手动打开并使用 DbConnection
或其他操作 ADO.NET 状态,则需要在将上下文实例返回到池之前还原该状态,例如关闭连接。 未能这样做可能会导致状态在不相关的请求中泄露。
编译的查询
当 EF 收到要执行的 LINQ 查询树时,它必须首先“编译”该树,例如从其生成 SQL。 由于此任务非常繁重,EF 根据查询树形状缓存查询,因此具有相同结构的查询将重复使用内部缓存的编译输出。 此缓存可确保多次执行相同的 LINQ 查询非常快,即使参数值不同。
但是,EF 仍必须执行某些任务,然后才能使用内部查询缓存。 例如,查询的表达式树必须与缓存查询的表达式树进行递归比较,才能找到正确的缓存查询。 在大多数 EF 应用程序中,此初始处理的开销可以忽略不计,特别是与与查询执行相关的其他成本(网络 I/O、实际查询处理和数据库中的磁盘 I/O……)相比。但是,在某些高性能场景中,可能需要消除此项。
EF 支持已编译的查询,这允许将 LINQ 查询显式编译为 .NET 委托。 获取此委托后,可以直接调用它来执行查询,而无需提供 LINQ 表达式树。 此方法会绕过缓存查找,并提供在 EF Core 中执行查询的最佳方式。 下面是比较编译和非编译查询性能的一些基准测试结果:在作出任何决定之前,先对平台进行基准测试。 此处提供了源代码,请根据需要将它用作自己的度量的基础。
方法 | NumBlogs | 平均值 | 错误 | 标准偏差 | Gen 0 | 已分配 |
---|---|---|---|---|---|---|
WithCompiledQuery | 1 | 564.2 us | 6.75 us | 5.99 us | 1.9531 | 9 KB |
WithoutCompiledQuery | 1 | 671.6 us | 12.72 us | 16.54 us | 2.9297 | 13 KB |
WithCompiledQuery | 10 | 645.3 us | 10.00 us | 9.35 us | 2.9297 | 13 KB |
WithoutCompiledQuery | 10 | 709.8 us | 25.20 us | 73.10 us | 3.9063 | 18 KB |
若要使用已编译的查询,请先编译 EF.CompileAsyncQuery 查询, (用于 EF.CompileQuery 同步查询) :
private static readonly Func<BloggingContext, int, IAsyncEnumerable<Blog>> _compiledQuery
= EF.CompileAsyncQuery(
(BloggingContext context, int length) => context.Blogs.Where(b => b.Url.StartsWith("http://") && b.Url.Length == length));
在此代码示例中,我们向 EF 提供接受 DbContext
实例的 lambda,以及要传递给查询的任意参数。 现在,只要想执行查询,就可以调用该委托:
await foreach (var blog in _compiledQuery(context, 8))
{
// Do something with the results
}
请注意,委托是线程安全的,可以在不同的上下文实例上并发调用。
限制
- 编译的查询只能用于单个 EF Core 模型。 相同类型的不同上下文实例有时可以配置为使用不同的模型;不支持在此场景中运行已编译的查询。
- 在编译的查询中使用参数时,请使用简单的标量参数。 不支持更复杂的参数表达式(例如对实例的成员/方法访问)。
查询缓存和参数化
当 EF 收到要执行的 LINQ 查询树时,它必须首先“编译”该树,例如从其生成 SQL。 由于此任务非常繁重,EF 根据查询树形状缓存查询,因此具有相同结构的查询将重复使用内部缓存的编译输出。 此缓存可确保多次执行相同的 LINQ 查询非常快,即使参数值不同。
请考虑以下两个查询:
var post1 = context.Posts.FirstOrDefault(p => p.Title == "post1");
var post2 = context.Posts.FirstOrDefault(p => p.Title == "post2");
由于表达式树包含不同的常量,因此表达式树是不同的,EF Core 将分别编译每个查询。 此外,每个查询生成一个略有不同 SQL 命令:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog1'
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = N'blog2'
由于 SQL 不同,因此数据库服务器可能还需要为这两个查询生成查询计划,而不是重新使用同一个计划。
对查询进行少量修改可能会显著更改内容:
var postTitle = "post1";
var post1 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
postTitle = "post2";
var post2 = context.Posts.FirstOrDefault(p => p.Title == postTitle);
由于博客名称现已参数化,因此两个查询具有相同的树形状,并且 EF 只需编译一次。 生成的 SQL 参数化,允许数据库重复使用相同的查询计划:
SELECT TOP(1) [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] = @__blogName_0
请注意,无需每个查询进行参数化:使用常量进行一些查询是非常好的,事实上,数据库(和 EF)有时可以针对常量执行某些优化,而这些优化在查询进行参数化时是不可能的。
备注
EF Core 的事件计数器报告查询缓存命中率。 在正常的应用程序中,当大多数查询至少执行了一次后,此计数器在程序启动后很快就会达到 100%。 如果此计数器仍稳定在 100% 以下,则表明应用程序可能正在执行一些会破坏查询缓存的操作,建议对此进行调查。
备注
数据库如何管理缓存查询计划取决于数据库。 例如,SQL Server 隐式维护 LRU 查询计划缓存,而 PostgreSQL 不会(但已准备的语句可能会产生非常相似的结果)。 有关详细信息,请参阅数据库文档。
动态构造的查询
在某些情况下,需要动态构造 LINQ 查询,而不是在源代码中直接指定它们。 例如,这可能发生在一个网站中,该网站从客户端接收任意查询详细信息,使用开放式查询操作符(排序、筛选、分页……)。在原理上,如果正确执行,动态构造的查询可以与常规查询一样高效(尽管无法对动态查询使用编译后的查询优化)。 但是,在实践中,它们通常是性能问题的来源,因为很容易意外生成每次形状都不同的表达式树。
下面的示例使用两种技术动态构造查询;仅在给定参数不为 null 时,才向查询添加 Where
运算符。 请注意,这不是动态构造查询的一个很好的用例,但为简单起见,我们将使用它:
- 常量
- 参数
[Benchmark]
public int WithConstant()
{
return GetBlogCount("blog" + Interlocked.Increment(ref _blogNumber));
static int GetBlogCount(string url)
{
using var context = new BloggingContext();
IQueryable<Blog> blogs = context.Blogs;
if (url is not null)
{
var blogParam = Expression.Parameter(typeof(Blog), "b");
var whereLambda = Expression.Lambda<Func<Blog, bool>>(
Expression.Equal(
Expression.MakeMemberAccess(
blogParam,
typeof(Blog).GetMember(nameof(Blog.Url)).Single()
),
Expression.Constant(url)),
blogParam);
blogs = blogs.Where(whereLambda);
}
return blogs.Count();
}
}
对这两种技术进行基准测试会得到以下结果:
方法 | 平均值 | 错误 | 标准偏差 | Gen 0 | Gen 1 | Gen 2 | 已分配 |
---|---|---|---|---|---|---|---|
WithConstant | 1,096.7 us | 12.54 us | 11.12 us | 13.6719 | 1.9531 | - | 83.91 KB |
WithParameter | 570.8 us | 42.43 us | 124.43 us | 5.8594 | - | - | 37.16 KB |
即使子毫秒差看起来很小,但请记住,常量版本会持续使缓存变慢,并会导致其他查询重新编译,从而降低它们的速度。
备注
除非确实需要,否则请避免使用表达式树 API 构造查询。 除了 API 的复杂性外,使用 API 时很容易在无意中导致严重的性能问题。
已编译的模型
备注
EF Core 6.0 版中引入了编译后的模型。
已编译的模型可以加快具有大型模型的应用程序的 EF Core 启动时间。 大型模型通常是指数百到数千种实体类型和关系。 这里的启动时间是在应用程序中首次使用 DbContext
类型时对 DbContext
执行首次操作的时间。 请注意,仅创建 DbContext
实例不会导致初始化 EF 模型。 相反,会导致模型初始化的典型首次操作包括调用 DbContext.Add
或执行第一个查询。
使用 dotnet ef
命令行工具创建已编译的模型。 在继续之前,请确保已安装了该工具的最新版本。
新 dbcontext optimize
命令用于生成已编译的模型。 例如:
dotnet ef dbcontext optimize
--output-dir
和 --namespace
选项可用于指定将在其中生成已编译的模型的目录和命名空间。 例如:
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>
- 如果你更习惯使用 Visual Studio,则还可使用 Optimize-DbContext
运行此命令的输出包含一段代码,可将其复制并粘贴到 DbContext
配置,以让 EF Core 使用已编译的模型。 例如:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseModel(MyCompiledModels.BlogsContextModel.Instance)
.UseSqlite(@"Data Source=test.db");
编译模型启动
通常不需要查看生成的启动代码。 但有时,这样做有助于对模型或其加载方式进行自定义。 启动代码看起来如下所示:
[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
private static BlogsContextModel _instance;
public static IModel Instance
{
get
{
if (_instance == null)
{
_instance = new BlogsContextModel();
_instance.Initialize();
_instance.Customize();
}
return _instance;
}
}
partial void Initialize();
partial void Customize();
}
这是一个包含分部方法的分部类,可实现这些方法来根据需要自定义模型。
此外,还可根据某些运行时配置,为可能使用不同模型的 DbContext
类型生成多个编译模型。 它们应置于不同的文件夹和命名空间中,如上所示。 然后,可以检查运行时信息(如连接字符串),并根据需要返回正确的模型。 例如:
public static class RuntimeModelCache
{
private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
= new();
public static IModel GetOrCreateModel(string connectionString)
=> _runtimeModels.GetOrAdd(
connectionString, cs =>
{
if (cs.Contains("X"))
{
return BlogsContextModel1.Instance;
}
if (cs.Contains("Y"))
{
return BlogsContextModel2.Instance;
}
throw new InvalidOperationException("No appropriate compiled model found.");
});
}
限制
已编译的模型有一些限制:
- 不支持全局查询筛选器。
- 不支持延迟加载和更改跟踪代理。
- 在模型定义或配置更改时,必须重新生成模型来手动同步模型。
- 不支持自定义 IModelCacheKeyFactory 实现。 但是,可以编译多个模型,并根据需要加载相应的模型。
由于这些限制,只应在 EF Core 启动时间太慢时使用已编译的模型。 编译小型模型通常不太值得使用已编译的模型。
如果支持其中的任何功能对你的成功至关重要,那么请为上面链接的相应问题投票。
降低运行时开销
与任何层一样,EF Core 与直接针对较低级别数据库 Api 编码相比,增加了一些运行时开销。 此运行时开销不太可能会显著影响大多数实际应用程序;本性能指南中的其他主题(如查询效率、索引使用情况和最小化往返)更为重要。 此外,即使对于高度优化的应用程序,网络延迟和数据库 i/o 通常会占用 EF Core 本身的所有时间。 但是,对于高性能、低延迟的应用程序,如果每个性能都非常重要,则使用以下建议将 EF Core 开销降到最低:
- 打开 DbContext 池我们的基测试表明,此功能可能会对高性能、低延迟的应用程序产生决定性影响。
- 请确保
maxPoolSize
符合使用场景;如果此值太低,DbContext
实例将被不断地创建和释放,从而降低性能。 如果将它设置得过高,则可能不必要地消耗内存,因为未使用的DbContext
实例仍保留在池中。 - 为了增加极小的性能,请考虑使用
PooledDbContextFactory
而不是让 DI 直接插入上下文实例(EF Core 6 及更高版本)。 对DbContext
池的 DI 管理会产生轻微的开销。
- 请确保
- 使用预编译查询进行热查询。
- LINQ 查询越复杂,它包含的运算符越多,生成的表达式树越大,使用编译查询的收益就越大。
- 请考虑在上下文配置(EF Core 6 及更高版本)中将
EnableThreadSafetyChecks
设置为 false 来禁用线程安全检查。- 不支持从不同的线程并发地使用相同的
DbContext
实例。 EF Core 具有一项安全功能,该功能可在许多情况下(但并不是全部)检测此编程 bug,并且会立即引发信息性异常。 但是,此安全功能增加了一些运行时开销。 - 警告:仅在全面测试应用程序不包含此类并发 bug 后禁用线程安全检查。
- 不支持从不同的线程并发地使用相同的