CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
ASP.NET Core 实战入门/课时/Entity Framework Core
RawBlame
课时目录12 节

ASP.NET Core 实战入门

  1. 01ASP.NET Core 基础
  2. 02C# 与 .NET 基础
  3. 03Entity Framework Core
  4. 04依赖注入
  5. 05REST / GraphQL / gRPC
  6. 06数据存储与缓存
  7. 07SignalR 实时通信
  8. 08测试与 E2E
  9. 09消息队列
  10. 10微服务与网关
  11. 11日志、任务与工具链
  12. 12综合练习

Entity Framework Core

Code First、迁移、Change Tracker 与 Dapper。

40 分钟

EF Core 8 数据访问(对照 JPA)

这一课你将能做到

  1. 用 Code First 方式建好题目/提交两张关联表并完成首次迁移
  2. 写出会翻译成 SQL 的查询,识别并消灭 N+1 查询
  3. 把 JPA/Hibernate 的核心概念(实体状态、懒加载、事务)逐一映射到 EF Core

为什么要学

EF Core 之于 .NET,就是 JPA/Hibernate 之于 Java:对象关系映射、工作单元、变更跟踪,一一对应。你已经理解的 JPA 知识——实体生命周期、JOIN FETCH、脏检查、事务边界——全部可以平移,需要重学的是 EF 特有的「迁移自动生成」和「LINQ 直译 SQL」。

人话:EF Core 是把 C# 对象和数据库行互相翻译的翻译官,SaveChanges() 就是提交批量翻译结果。Spring Data JPA 的 JpaRepository 帮你写好了 CRUD,EF Core 用 DbSet<T> 承担同样角色,但查询语言统一用 LINQ,不再区分 JPQL。

先建立图像

Java 侧:  Repository ─→ EntityManager(Persistence Context) ─→ JDBC ─→ DB
C#   侧:  DbSet<T>   ─→ DbContext(ChangeTracker)         ─→ ADO.NET ─→ DB
                    同一个抽象层次,连实体状态机都同名同义

实体状态对照:

JPA 状态EF Core 状态含义
TransientDetached未被上下文跟踪
ManagedUnchanged/Modified已跟踪,提交时更新
RemovedDeleted待删除
New/AttachedAdded待插入

跟着做

步骤 1:安装包并定义实体

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
public class Problem
{
    public long Id { get; set; }
    public string Title { get; set; } = null!;
    public string Slug { get; set; } = null!;
    public Difficulty Difficulty { get; set; }
    public bool IsDeleted { get; set; }
    public List<Submission> Submissions { get; set; } = [];
}

public class Submission
{
    public long Id { get; set; }
    public long ProblemId { get; set; }
    public Problem? Problem { get; set; }      // 导航属性
    public string UserName { get; set; } = null!;
    public string Status { get; set; } = null!;
    public int TimeMs { get; set; }
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}

public enum Difficulty { Easy, Medium, Hard }

EF Core 按约定映射:Id 为主键,ProblemId + 导航属性自动识别一对多,Difficulty 枚举存整数(EF Core 8 起也可存字符串)。

步骤 2:定义 DbContext 并注册

public class OjDbContext(DbContextOptions<OjDbContext> options) : DbContext(options)
{
    public DbSet<Problem> Problems => Set<Problem>();
    public DbSet<Submission> Submissions => Set<Submission>();

    protected override void OnModelCreating(ModelBuilder mb)
    {
        mb.Entity<Problem>(e =>
        {
            e.HasIndex(p => p.Slug).IsUnique();          // 唯一索引
            e.HasQueryFilter(p => !p.IsDeleted);         // 全局软删除过滤,等价 Hibernate @Where
        });
        mb.Entity<Submission>()
          .HasOne(s => s.Problem)
          .WithMany(p => p.Submissions)
          .HasForeignKey(s => s.ProblemId);
    }
}

// Program.cs —— 对应 @EntityScan + DataSource 装配
builder.Services.AddDbContext<OjDbContext>(opt => opt.UseSqlite("Data Source=oj.db"));

步骤 3:迁移(Code First,EF 的招牌能力)

dotnet ef migrations add InitSchema
dotnet ef database update

EF 对比实体定义与快照,自动生成建表 SQL(Migrations/xxx_InitSchema.cs,可手改)。Java 侧 Flyway/Liquibase 是「人写 SQL、工具执行版本」,EF 是「工具读模型生成 SQL、人审核」——方向相反,目标相同。

步骤 4:CRUD 五连

// 查询(自动翻译成 SELECT ... WHERE ... ORDER BY)
List<Submission> recent = await db.Submissions
    .Where(s => s.Status == "AC" && s.TimeMs < 1000)
    .OrderByDescending(s => s.CreatedAt)
    .Take(20)
    .AsNoTracking()                    // 只读查询跳过变更跟踪,等价 Hibernate 无状态读
    .ToListAsync();

// 新增
db.Submissions.Add(new Submission { ProblemId = 1, UserName = "alice", Status = "WA" });
await db.SaveChangesAsync();           // 工作单元提交,等价 flush + commit

// 修改:先查出(被跟踪),改属性,SaveChanges 生成 UPDATE —— 这就是脏检查
var p = await db.Problems.FirstAsync(p => p.Id == 1);
p.Title = "两数之和 II";
await db.SaveChangesAsync();

// .NET 7+ 批量更新:一条 UPDATE 语句,不加载实体
await db.Submissions.Where(s => s.Id == 5)
    .ExecuteUpdateAsync(set => set.SetProperty(s => s.Status, "TLE"));

步骤 5:N+1 治理(对照 JOIN FETCH)

// ❌ N+1:列表页循环访问 s.Problem.Title,每条触发一次 SELECT
var bad = await db.Submissions.ToListAsync();

// ✅ 预加载:一条 SQL(LEFT JOIN),等价 @Query + JOIN FETCH
var good = await db.Submissions
    .Include(s => s.Problem)
    .Where(s => s.CreatedAt > DateTime.UtcNow.AddDays(-1))
    .ToListAsync();

// ✅ 投影:只取需要列,连 JOIN 都省了,列表页首选
var best = await db.Submissions
    .Select(s => new { s.UserName, s.Status, ProblemTitle = s.Problem!.Title })
    .ToListAsync();

步骤 6:事务(多操作原子提交)

await using var tx = await db.Database.BeginTransactionAsync();
try
{
    db.Problems.Add(newProblem);
    await db.SaveChangesAsync();
    await db.Submissions.AddAsync(firstSubmission);
    await db.SaveChangesAsync();
    await tx.CommitAsync();
}
catch
{
    await tx.RollbackAsync();
    throw;
}

单次 SaveChangesAsync 本身就是事务(等价 JPA 一次 flush),只有跨多次 SaveChanges 才需要显式事务,规则与 Spring @Transactional 一致。

讲清楚要点

  1. ChangeTracker 是 EF Core 的心脏:查询到的实体会被上下文跟踪,修改属性后 SaveChanges 自动算出增量 UPDATE。代价是内存与 CPU,因此「只读列表一律 AsNoTracking()」是性能铁律(可用 QueryTrackingBehavior.NoTracking 全局开启)。
  2. DbContext 不是线程安全的:一个请求一个实例(AddDbContext 默认 Scoped),多个请求共享会炸。后台线程用 IDbContextFactory<T> 创建独立实例。
  3. LINQ 直译 SQL,翻译不了就抛异常:Where 里不能调用自定义 C# 方法(数据库不认识),能用 string.Contains、DateTime.Compare 等受支持函数,不能用的逻辑放到 ToListAsync() 之后。
  4. 全局查询过滤器解决软删除:HasQueryFilter 让所有查询自动带 WHERE IsDeleted = 0,等价 Hibernate @Where + Spring Data 的自定义实现,业务代码零感知。

你可能会懵的地方

  • EF Core 有没有 Spring Data JPA 的方法名派生查询(findByStatusAndTime)? 不需要。LINQ 表达式本身就是「内联的派生查询」,类型安全且支持任意组合,EF 团队认为这比方法名语法表达力强得多。

  • 懒加载默认开启吗? 默认关闭(这点和 JPA 默认 LAZY 不同)。导航属性不 Include 就是 null,访问集合导航(未加载时)不会隐式触发查询。开启需装 Microsoft.EntityFrameworkCore.Proxies 并标 virtual,实践中推荐显式 Include/投影,行为可预测。

  • 迁移文件冲突了怎么办(团队协作)? 两个人基于同一快照各自生成迁移,后提交者 rebase 后重新 dotnet ef migrations remove 再生成。规则:迁移历史表(__EFMigrationsHistory)里已有的迁移不可改,只能新增。

  • SaveChanges 和 SaveChangesAsync 会不会自动开事务? 会。单次调用内的所有 INSERT/UPDATE/DELETE 在一个事务里原子提交,任一失败全部回滚——与 JPA 一次 flush 的事务语义一致。

  • 原生 SQL 怎么写? db.Database.SqlQuery<T>($"...") 或 FromSql,支持参数化防注入。等价 JPA 的 nativeQuery。EF Core 8 还支持未映射类型直接映射查询结果。

错法对照

错法问题正解
列表查询不写 AsNoTracking()白白跟踪实体,内存翻倍只读场景统一 AsNoTracking 或全局配置
循环里逐条 Add + SaveChangesN 次事务往返攒批后一次 AddRange + 一次 SaveChanges
在 Where(s => Encrypt(s.Code) == x) 用自定义方法无法翻译,运行时异常先查出再内存比较,或用数据库函数映射
修改实体前忘了查询直接 new 一个赋 Id 再 Update状态混乱查出改属性;或 db.Update(entity) 全量覆盖(明确知道全字段时)
DbContext 注入到 Singleton 服务跨请求共享非线程安全实例服务改 Scoped,或用 IDbContextFactory 按需创建

卡点急救

  1. dotnet ef 命令找不到:dotnet tool install --global dotnet-ef,并确认 ~/.dotnet/tools 在 PATH 里;项目还需引用 Microsoft.EntityFrameworkCore.Design。
  2. 模型改了但查询结果没变:检查是否 AsNoTracking + 同一上下文内旧实例缓存;或改了实体但没执行新迁移(dotnet ef migrations add + database update)。
  3. SQLite 本地跑通、换 SQL Server 报排序规则/类型错误:迁移是按提供程序生成的,切换数据库需重新生成迁移目录,不能混用。

面试常问

  • EF Core 和 JPA/Hibernate 的核心异同? 相同:工作单元模式、变更跟踪(脏检查)、级联、延迟可配置、事务边界一致。不同:EF 的查询语言是 LINQ(编译期类型安全,可同时作用于内存与数据库),JPA 用 JPQL/Criteria;EF 迁移可从模型自动生成,Flyway/Liquibase 靠人工维护 SQL;EF 默认无懒加载代理,JPA 默认 LAZY。

  • EF Core 如何避免 N+1?和 JPA 手段对比? 三招:Include 预加载(对应 JOIN FETCH)、投影 Select 只取所需列(等价 DTO 投影查询)、拆分查询 AsSplitQuery(一条 SQL 拆成多条,避免笛卡尔积,JPA 侧靠 @BatchSize/IN 批量抓取)。诊断靠日志输出 ToQueryString() 看生成 SQL,等价 Hibernate show_sql。

  • ChangeTracker 的实体状态流转? Detached(未跟踪)→ Added(Add 调用,SaveChanges 转 Unchanged);查询出的实体 Unchanged → 属性修改后 Modified;Remove 后 Deleted。SaveChanges 按状态生成 SQL 并把已提交实体重置 Unchanged。这与 JPA 的 Transient/Managed/Removed/Detached 生命周期几乎逐字对应。

自测三问

  1. 为什么只读查询要 AsNoTracking()?代价和收益各是什么?
  2. Include 和投影 Select 各适用什么场景?
  3. 单次 SaveChangesAsync 的原子性由什么保证?

小结一句话

EF Core = JPA 的概念 + LINQ 的查询语言 + 自动生成迁移的工程化红利,抓住 ChangeTracker、AsNoTracking、Include 三件事就能写出不慢的数据层。

延伸

  • 官方文档:EF Core 概述
  • 下一课:依赖注入与 IoC 容器——三种生命周期、Keyed Services,对照 Spring 注解体系。
加载本节练习…
← 上一课C# 与 .NET 基础下一课 →依赖注入