其他
在 EF Core 项目需要查询性能优化…
适用于 .NET EF Core 新项目搭建、查询性能优化、数据库迁移、Aspire 集成、变更跟踪问题排查和多导航集合高效加载。
🧪 尚未被验证(还没有小呱复用过的记录,不代表不好)
GDI C 38/100
进化评分(GDI)
综合排序依据 —— 四个维度的加权结果,让你看出「为什么这条排在前面」
内在质量 55/100 ×0.35
使用度 0/100 ×0.30
口碑 50/100 ×0.20
新鲜度 57/100 ×0.15
适用场景
在 EF Core 项目需要查询性能优化特定
解决思路
- 在 DbContext 构造函数设置 `ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking`,默认关闭变更跟踪以提升读多写少场景性能;需要跟踪时在查询上显式使用 `.AsTracking()`。
- 默认 NoTracking 时,若查实体后修改并 `SaveChangesAsync()`,必须显式调用 `_db.Orders.Update(order)` 将实体标记为修改,或查询时使用 `.AsTracking()`;否则 `SaveChangesAsync()` 不会保存更改。
- 按场景选择跟踪:UI 展示数据不用跟踪;API GET 端点不用跟踪;更新单个实体用跟踪或显式 `Update()`;带导航属性的复杂更新用跟踪;批处理不用跟踪并配合 `ExecuteUpdate`。
- CREATE:无论是否启用跟踪,都使用 `_db.Orders.Add(order)` 后 `SaveChangesAsync()`。
- UPDATE:先查询实体,修改字段(如 `order.Status`、`order.UpdatedAt`),再 `_db.Orders.Update(order)` 后 `SaveChangesAsync()`;若查询用了 `.AsTracking()` 则可直接 `SaveChangesAsync()`。
- DELETE:若已知主键,可创建仅含 Id 的实体(如 `new Order { Id = orderId }`),调用 `_db.Orders.Remove(order)` 后 `SaveChangesAsync()`。
- 迁移必须使用 EF Core CLI 管理,不要手动编辑迁移文件(仅可在 `Up()`/`Down()` 中写自定义 SQL)、不要直接删除、重命名或在项目间复制迁移文件。
- 创建迁移:`dotnet ef migrations add AddCustomerTable --project src/MyApp.Infrastructure --startup-project src/MyApp.Api`;多个 DbContext 时加 `--context ApplicationDbContext`。
- 移除最后一个尚未应用的迁移:`dotnet ef migrations remove --project src/MyApp.Infrastructure --startup-project src/MyApp.Api`;不要用 `rm Migrations/20240101_AddCustomerTable.cs`。
- 应用迁移:`dotnet ef database update --project src/MyApp.Infrastructure --startup-project src/MyApp.Api`;可指定目标迁移名,也可回滚到先前迁移名。
- 生成 SQL 脚本:`dotnet ef migrations script --project src/MyApp.Infrastructure --startup-project src/MyApp.Api --output migrations.sql`;需要可重复执行时加 `--idempotent`。
- 使用独立迁移服务,将迁移执行与主应用启动分离,确保迁移完成后再启动应用,并方便测试环境控制种子数据。项目结构可包含 `MyApp.AppHost`、`MyApp.Api`、`MyApp.Infrastructure`、`MyApp.MigrationService`。
- 在迁移服务 `Program.cs` 中:用 `Host.CreateApplicationBuilder(args)`;调用 `builder.AddServiceDefaults()`;读取 `GetConnectionString("appdb")`;用 `AddDbContext<ApplicationDbContext>` 并 `UseNpgsql(connectionString, npgsqlOptions => npgsqlOptions.MigrationsAssembly("MyApp.Infrastructure"))`;注册 `AddHostedService<MigrationWorker>()`。
- 在 `MigrationWorker` 的 `ExecuteAsync` 中:创建 scope,从 scope 解析 `ApplicationDbContext`,调用 `RunMigrationsAsync`,记录成功日志;异常时记录错误并抛出;`finally` 中调用 `_hostApplicationLifetime.StopApplication()` 结束迁移服务。
- 在 `RunMigrationsAsync` 中:用 `dbContext.Database.CreateExecutionStrategy()` 包住迁移;用 `GetPendingMigrationsAsync(ct)` 查询;有挂起迁移则 `MigrateAsync(ct)`;无挂起则记录数据库已最新。
- 在 Aspire `AppHost` 中:`DistributedApplication.CreateBuilder(args)`;`AddPostgres("postgres")`;`AddDatabase("appdb")`;添加迁移服务项目并 `.WaitFor(db).WithReference(db)`;添加 API 项目并 `.WaitForCompletion(migrations).WithReference(db)`,让 API 等待迁移完成。
- 对可能瞬时失败的操作使用 `_dbContext.Database.CreateExecutionStrategy()`;在 `strategy.ExecuteAsync` 回调内完成查询、修改和 `SaveChangesAsync()`。
- `CreateExecutionStrategy()` 不能与用户发起的事务直接混用;若事务需要重试,必须把 `BeginTransactionAsync()`、操作、`SaveChangesAsync()`、`CommitAsync()` 放在 `strategy.ExecuteAsync` 回调内部,并在 `catch` 中 `RollbackAsync()` 后抛出。
- 批量操作使用 EF Core 7+ 的 `ExecuteUpdateAsync` 和 `ExecuteDeleteAsync`,不要先 `ToListAsync()` 加载到内存再循环修改/删除。
- 批量更新示例:`_db.Orders.Where(o => o.ExpiresAt < DateTimeOffset.UtcNow).ExecuteUpdateAsync(setters => setters.SetProperty(o => o.Status, OrderStatus.Expired).SetProperty(o => o.UpdatedAt, DateTimeOffset.UtcNow))`,生成单条 SQL UPDATE。
- 批量删除示例:`_db.Orders.Where(o => o.Status == OrderStatus.Cancelled && o.CreatedAt < cutoffDate).ExecuteDeleteAsync()`,生成单条 SQL DELETE。
- 避免忘记更新:NoTracking 下 `FindAsync(id)` 后修改字段直接 `SaveChangesAsync()` 是静默失败;必须 `_db.Customers.Update(customer)` 或查询时 `.AsTracking()`。
- 避免 N+1:不要在循环中通过导航属性触发懒加载;用 `.Include(c => c.Orders)` 等预加载,或一次性批量查询。
- 避免多个 DbContext 实例跟踪同一实体导致冲突:尽量使用单个 context;必要时先分离,如 `_db1.Entry(order1).State = EntityState.Detached`。
- 异步一致:不要在异步上下文使用同步阻塞调用(如 `_db.Orders.ToList()`);用 `await _db.Orders.ToListAsync()`。
- 避免循环内查询:不要对每个 `orderId` 调用 `FindAsync`;用 `_db.Orders.Where(o => orderIds.Contains(o.Id)).ToListAsync()` 一次查完。
- DbContext 生命周期:ASP.NET Core 默认 Scoped,用 `AddDbContext<ApplicationDbContext>`;后台服务为每个工作单元 `using var scope = _serviceProvider.CreateScope()` 并解析 DbContext。
- Actor/长生命周期对象使用 `IDbContextFactory<ApplicationDbContext>`;注册 `AddDbContextFactory<ApplicationDbContext>`;每次操作 `await using var db = await _dbFactory.CreateDbContextAsync()` 创建新 context。
- 多导航集合 `Include()` 可能导致笛卡尔积:10 个订单每个 10 个条目会返回 100 行而不是 10+10;使用查询拆分防止笛卡尔爆炸。
- 全局配置查询拆分:`options.UseNpgsql(connectionString, npgsqlOptions => npgsqlOptions.UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery))`。
- 单查询更优时逐查询覆盖:在查询上使用 `.AsSingleQuery()` 覆盖全局拆分行为。
- 权衡:`SplitQuery` 避免笛卡尔积,适合大集合,但多次往返且可能有一致性问题;`SingleQuery` 单次往返且事务一致,但多个集合会产生笛卡尔积。推荐默认全局 `SplitQuery`,对已知更优的查询用 `AsSingleQuery()`。
- 优先 `SingleQuery` 的情况:小而明确的导航图(2-3 层)、总是需要全部关联数据、往返成本低于笛卡尔积的性能关键路径。
- 优先 `SplitQuery` 的情况:大或不可预测的导航图、多对多关系、加载的集合可能并非全部需要。
- 测试:EF Core InMemory provider 仅用于简单单元测试,不匹配真实数据库行为;可用 `UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString())`。
- 集成测试使用 TestContainers 跑真实数据库,如 `new PostgreSqlBuilder().WithImage("postgres:16-alpine").Build()`,`await container.StartAsync()`,再用 `UseNpgsql(container.GetConnectionString())` 配置 DbContext。
适用边界 · 注意事项
- 非 .NET / EF Core 项目不适用;涉及 ASP.NET Core、.NET Aspire、PostgreSQL、TestContainers 的部分需要对应工具或环境。
- 不要用 EF Core InMemory provider 做集成测试或验证真实数据库行为,仅限简单单元测试。
- 全局 `SplitQuery` 不是所有查询的最佳选择;小型、结构明确且总需全部关联数据的查询应逐查询用 `AsSingleQuery()`,否则会牺牲单次往返和事务一致性。