醋醋百科网

Good Luck To You!

EF Core 最佳实践

Entity Framework Core 是一个强大而灵活的对象关系映射 (O/RM) 框架。遵循最佳实践可以帮助你构建高性能、可维护且可靠的应用程序。

1. DbContext 生命周期管理

使用短生命周期

DbContext 被设计为轻量级、一次性的工作单元。

  • Web 应用: 在 ASP.NET Core 中,默认使用作用域 (Scoped) 生命周期。即每个 HTTP 请求创建一个新的 DbContext 实例。这是推荐的方式,通过 services.AddDbContext<YourDbContext>(...) 注册。
  • 其他应用: 对于非 Web 应用(如控制台、WPF、WinForms),通常在需要执行数据库操作的业务单元(例如一个方法或一个 using 块)内创建和释放 DbContext 实例。
  • 示例 (非 Web):
using (var context = new YourDbContext())
{
    // 执行数据库操作
    var blogs = context.Blogs.ToList();
    // ...
    context.SaveChanges();
} // context 在这里被 Dispose

避免长生命周期

不要将 DbContext 注册为单例 (Singleton)。这会导致内存泄漏(因为 DbContext 会跟踪越来越多的实体)、数据陈旧以及并发问题。

使用 DbContext 池 (可选)

对于高吞吐量场景,可以使用 AddDbContextPool<YourDbContext>(...)。它通过重置状态来复用 DbContext 实例,可以减少实例化的开销。但要注意,如果 DbContext 的构造函数或 OnConfiguring 中有状态管理,可能会有问题。


2. 高效查询 (核心性能)

只查询所需数据

  • Select() 投影: 使用 LINQ 的 Select() 方法只选择你需要的列,而不是加载整个实体。这可以显著减少网络流量和内存占用。
var blogTitles = await context.Blogs
    // 只选择 Title 列
    .Select(b => b.Title) 
    .ToListAsync();
  • DTO 投影: 将查询结果直接投影到数据传输对象 (DTO)。
var blogDtos = await context.Blogs
    .Select(b => new BlogDto 
    { 
        Id = b.BlogId, Name = b.Title 
    })
    .ToListAsync();

使用AsNoTracking()进行只读查询

如果你只是读取数据并且不打算修改它们(即不需要 SaveChanges()),请使用 .AsNoTracking()。这会禁用变更跟踪,显著提高查询性能并减少内存使用。

var blogs = await context.Blogs
    .AsNoTracking()
    .ToListAsync();

避免 N+1 问题

当加载关联数据时,注意避免 N+1 查询(查询主实体,然后为每个主实体单独查询关联实体)。

  • 预加载 (Eager Loading): 使用 Include() 和 ThenInclude() 一次性加载所需的关联数据。适用于关联数据量可控的情况。
var blogsWithPosts = await context.Blogs
    // 加载 Posts
    .Include(b => b.Posts) 
    // 加载 Post 的 Author
    .ThenInclude(p => p.Author) 
    .ToListAsync();
  • 显式加载 (Explicit Loading): 在需要时手动加载关联数据。
var blog = await context.Blogs.FindAsync(1);
if (blog != null)
{
    // 手动加载 Posts
    await context.Entry(blog)
        .Collection(b => b.Posts)
        .LoadAsync(); 
}
  • 投影 (Projection): 通过 Select() 选择所需的主实体和关联实体数据,通常性能最好。

过滤和排序在数据库端执行

尽量在 ToList()、ToArray()、FirstOrDefault() 等“执行”查询的方法之前应用 Where(), OrderBy(), Skip(), Take() 等操作。这样可以让数据库执行过滤和排序,而不是在内存中处理大量数据。

// 好: 数据库执行过滤和排序
var recentBlogs = await context.Blogs
    .Where(b => 
        b.PublishedOn > DateTime.UtcNow.AddDays(-7)
    )
    .OrderByDescending(b => b.PublishedOn)
    .Take(10)
    .ToListAsync();

// 差: 先加载所有数据到内存,再过滤排序 (性能极差!)
// var allBlogs = await context.Blogs.ToListAsync();
// var recentBlogs = allBlogs.Where(...) /* ... */;

异步操作

优先使用异步方法 (ToListAsync(), FirstOrDefaultAsync(), SaveChangesAsync(), FindAsync(), etc.)。这可以释放当前线程,提高应用程序的响应性和吞吐量,尤其是在 Web 应用中。

警惕客户端评估

尽量避免 LINQ 查询在客户端(应用程序内存中)进行评估,因为这可能导致加载整个表到内存中。检查日志以确保查询在数据库端正确翻译和执行。

分页

使用 Skip() 和 Take() 实现高效的分页,确保 OrderBy() 在 Skip() 之前。

使用ExecuteUpdateAsync和ExecuteDeleteAsync(EF Core 7+)

对于批量更新或删除操作,这两个方法直接在数据库中执行,绕过了变更跟踪器,性能极高。

// 示例:批量更新
await context.Blogs
    .Where(b => b.Rating < 3)
    .ExecuteUpdateAsync(setters => setters
        .SetProperty(b => b.IsArchived, true)
    );

// 示例:批量删除
await context.Posts
    .Where(p => p.IsSpam)
    .ExecuteDeleteAsync();

3. 变更跟踪与保存

理解变更跟踪

DbContext 默认会跟踪从数据库查询出来的实体。当你修改实体属性时,DbContext 会记录这些变化。调用 SaveChanges() 或 SaveChangesAsync() 时,EF Core 会检测这些变化并生成相应的 INSERT, UPDATE, DELETE SQL 语句。

批量操作

如上所述,对于大量实体的更新或删除,优先考虑
ExecuteUpdateAsync/ExecuteDeleteAsync (EF Core 7+)。如果版本较低或操作复杂,可能需要分批次处理 (SaveChanges() 多次) 或使用原生 SQL 或 Dapper 等库。

附加 (Attach) 和更新

如果你有一个已存在于数据库但当前未被跟踪的实体(例如,从 Web 请求反序列化而来),并想更新它:

  • 方法一 (推荐): 先查询,再更新。
var existingBlog = await context.Blogs
    .FindAsync(blogDto.Id);
if (existingBlog != null)
{
    // 只更新需要的属性
    existingBlog.Title = blogDto.Title; 
    // ...
    await context.SaveChangesAsync();
}
  • 方法二 (Attach + Mark Modified): 如果不想先查询,可以附加实体并标记其状态。
// 可能只有部分属性
var blogToUpdate = new Blog { 
    BlogId = blogDto.Id, 
    Title = blogDto.Title 
}; 
// 将实体标记为 Modified,会更新所有列
context.Blogs.Update(blogToUpdate); 
// 或者更精细地控制:
// context.Attach(blogToUpdate);
// context.Entry(blogToUpdate)
//    .Property(b => b.Title).IsModified = true;
await context.SaveChangesAsync();

注意:context.Update() 会将实体的所有属性标记为已修改,导致生成更新所有列的 SQL,即使某些属性值没有变。


4. 数据模型设计

遵循约定

EF Core 有很多约定(如 Id 或 ClassNameId 作为主键,导航属性自动识别关系等)。遵循约定可以减少配置代码。

使用 Fluent API 或 Data Annotations

  • Fluent API: 在 OnModelCreating 方法中使用,提供最强大、最灵活的配置方式,将配置与实体类分离。推荐用于复杂的或共享的模型配置。
  • Data Annotations: 直接在实体类属性上使用特性 (Attributes),如 [Key], [Required], [MaxLength]。简单直观,但会使实体类与 EF Core 配置耦合。

配置索引

为经常用于查询条件 (WHERE) 或排序 (ORDER BY) 的列创建索引,以提高查询性能。使用 Fluent API 的 HasIndex() 方法。

protected override void OnModelCreating(
    ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .HasIndex(b => b.Url) // 为 Url 列创建索引
        .IsUnique(); // 可以指定唯一索引
}

值转换器 (Value Converters)

用于将属性类型在存入数据库时进行转换(例如,将 enum 存为字符串,或加密数据)。

影子属性 (Shadow Properties)

不在实体类中定义的属性,但存在于 EF Core 模型中(例如 LastUpdated 时间戳),可以通过 ChangeTracker API 访问。


5. 数据库迁移 (Migrations)

使用 Code-First Migrations

这是管理数据库架构变更的标准方式。

保持迁移的原子性和可审查性

每次迁移应对应一组相关的、逻辑上的变更。生成迁移后,务必审查生成的 C# 代码和 SQL 脚本。

迁移策略

  • 开发环境: 可以使用 Update-Database (Package Manager Console) 或 dotnet ef database update (CLI)。
  • 生产环境:生成 SQL 脚本: 使用 Script-Migration (PMC) 或 dotnet ef migrations script (CLI) 生成 SQL 脚本,然后由 DBA 审查并在合适的维护窗口执行。这是最安全的方式。应用程序启动时迁移 (谨慎使用): 调用 dbContext.Database.Migrate()。简单但不推荐用于生产,特别是在多实例部署时可能导致并发问题或启动延迟。如果使用,确保有适当的锁定机制。迁移包 (Migration Bundles) (EF Core 6+): 生成包含迁移逻辑的可执行文件,可以在部署过程中独立运行。

处理数据迁移

如果架构变更需要移动或转换现有数据,可能需要在迁移中编写原始 SQL (migrationBuilder.Sql("..."))。


6. 并发控制

理解乐观并发

EF Core 默认使用乐观并发。它假设并发冲突很少发生。在 SaveChanges() 时,如果检测到数据在你读取后已被其他人修改,会抛出
DbUpdateConcurrencyException。

使用并发令牌

配置一个属性作为并发令牌(通常是 rowversion/timestamp 类型的列,或任何你选择的属性)。EF Core 会在 UPDATE/DELETE 语句的 WHERE 子句中包含这个令牌的原始值。

// 使用 Data Annotation
public class Blog
{
    public int BlogId { get; set; }
    // ...
    // 对于 SQL Server,映射到 rowversion
    [Timestamp] 
    public byte[] RowVersion { get; set; }
}

// 或者使用 Fluent API
modelBuilder.Entity<Blog>()
    .Property(b => b.RowVersion)
    .IsRowVersion(); // 或 .IsConcurrencyToken();

处理并发异常

捕获
DbUpdateConcurrencyException 并采取适当的策略(例如,通知用户数据已更新,提供合并选项,或强制覆盖)。


7. 日志与错误处理

配置日志

在开发过程中启用详细日志记录,特别是 SQL 语句的日志,可以帮助诊断性能问题和理解 EF Core 的行为。使用
Microsoft.Extensions.Logging。

// 在 appsettings.Development.json 或代码中配置
services.AddDbContext<YourDbContext>(options =>
    options.UseSqlServer(connectionString)
        // 输出到控制台
        .LogTo(Console.WriteLine, LogLevel.Information) 
        // 开发环境可开启敏感数据日志
        .EnableSensitiveDataLogging()); 

处理特定异常

准备好处理常见的 EF Core 异常,如 DbUpdateException (保存失败,可能包含内部数据库错误) 和
DbUpdateConcurrencyException (并发冲突)。


8. 测试

  • 使用内存数据库 (谨慎): Microsoft.EntityFrameworkCore.InMemory 提供一个内存数据库提供程序,可用于单元测试或快速集成测试。但它不是关系数据库,行为(如事务、约束、某些 LINQ 翻译)可能与真实数据库不同。不推荐用它来测试数据库相关逻辑的正确性。
  • 使用 SQLite (内存模式): SQLite 可以在内存模式下运行,它是一个真正的关系数据库,比 InMemory 提供程序更接近真实场景,但仍与 SQL Server/PostgreSQL 等有差异。
  • 使用测试数据库 (推荐): 为了进行可靠的集成测试,最好针对与生产环境相同类型(或非常相似)的数据库进行测试。可以使用本地数据库实例、Docker 容器 (如 Testcontainers) 或专门的测试数据库。确保测试之间的数据隔离。
  • 仓库模式/服务层: 可以将 DbContext 封装在仓库 (Repository) 或服务层之后,这有助于解耦和模拟依赖项进行单元测试。

遵循这些最佳实践可以帮助你更有效地使用 EF Core,构建出性能优良、易于维护的应用程序。记住,没有绝对的“银弹”,具体实践需要根据项目需求和场景进行调整。

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言