CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
EF Core 8 数据访问(对照 JPA)
这一课你将能做到
用 Code First 方式建好题目/提交两张关联表并完成首次迁移
写出会翻译成 SQL 的查询,识别并消灭 N+1 查询
把 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 状态 含义 Transient Detached 未被上下文跟踪 Managed Unchanged/Modified 已跟踪,提交时更新 Removed Deleted 待删除 New/Attached Added 待插入
跟着做
步骤 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 一致。
讲清楚要点
ChangeTracker 是 EF Core 的心脏 :查询到的实体会被上下文跟踪,修改属性后 SaveChanges 自动算出增量 UPDATE。代价是内存与 CPU,因此「只读列表一律 AsNoTracking()」是性能铁律(可用 QueryTrackingBehavior.NoTracking 全局开启)。
DbContext 不是线程安全的 :一个请求一个实例(AddDbContext 默认 Scoped),多个请求共享会炸。后台线程用 IDbContextFactory<T> 创建独立实例。
LINQ 直译 SQL,翻译不了就抛异常 :Where 里不能调用自定义 C# 方法(数据库不认识),能用 string.Contains、DateTime.Compare 等受支持函数,不能用的逻辑放到 ToListAsync() 之后。
全局查询过滤器解决软删除 :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 + SaveChanges N 次事务往返 攒批后一次 AddRange + 一次 SaveChanges 在 Where(s => Encrypt(s.Code) == x) 用自定义方法 无法翻译,运行时异常 先查出再内存比较,或用数据库函数映射 修改实体前忘了查询直接 new 一个赋 Id 再 Update 状态混乱 查出改属性;或 db.Update(entity) 全量覆盖(明确知道全字段时) DbContext 注入到 Singleton 服务 跨请求共享非线程安全实例 服务改 Scoped,或用 IDbContextFactory 按需创建
卡点急救
dotnet ef 命令找不到 :dotnet tool install --global dotnet-ef,并确认 ~/.dotnet/tools 在 PATH 里;项目还需引用 Microsoft.EntityFrameworkCore.Design。
模型改了但查询结果没变 :检查是否 AsNoTracking + 同一上下文内旧实例缓存;或改了实体但没执行新迁移(dotnet ef migrations add + database update)。
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 生命周期几乎逐字对应。
自测三问
为什么只读查询要 AsNoTracking()?代价和收益各是什么?
Include 和投影 Select 各适用什么场景?
单次 SaveChangesAsync 的原子性由什么保证?
小结一句话 EF Core = JPA 的概念 + LINQ 的查询语言 + 自动生成迁移的工程化红利,抓住 ChangeTracker、AsNoTracking、Include 三件事就能写出不慢的数据层。
延伸