EF Core 基础入门

系统讲解 EF Core 的实体建模、DbContext、CRUD、迁移、关系映射、查询优化与常见实践。

EF Core 基础入门

对很多 .NET 后端开发者来说,Entity Framework Core 是最常接触的数据访问框架之一。它的核心价值,不只是“少写 SQL”,而是把对象模型、数据库结构、查询表达式和迁移管理串成一套相对统一的开发体验。

但也正因为它足够方便,很多初学者会在还没真正理解原理的情况下就开始无脑 Include、无脑查整表、无脑更新整对象,最后遇到性能和数据一致性问题就会很迷茫。

所以学习 EF Core 时,既要学会“怎么用”,也要理解“什么时候该谨慎用”。


第一章 EF Core 是什么

EF Core.NET 平台上的 ORM 框架之一。ORM 的核心思想是:

  • 用对象表示数据
  • 用代码表达查询和更新
  • 让程序和数据库之间的映射更自然

它主要解决的问题包括:

  • 减少重复的数据库操作样板代码
  • 统一实体映射方式
  • LINQ 表达查询
  • 管理数据库结构变更

它特别适合:

  • 常规业务系统
  • 后台管理系统
  • 以 CRUD 为主的 Web API 项目

第二章 必须先掌握的三个核心概念

入门 EF Core 时,最重要的不是先背一堆 API,而是先抓住这三个对象。

2.1 实体类

实体类通常对应数据库中的表记录。

public class User
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
}

2.2 DbSet<T>

DbSet<T> 可以理解成某张表在代码层面的集合入口。

2.3 DbContext

DbContext 是数据库操作入口,负责:

  • 管理实体集合
  • 跟踪实体状态
  • 执行查询
  • 保存变更

示例:

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
    {
    }

    public DbSet<User> Users => Set<User>();
}

可以先这样记:

实体类 -> 表中的一条记录
DbSet<T> -> 一张表
DbContext -> 整个数据库操作上下文

第三章 如何把 EF Core 接进 ASP.NET Core

3.1 安装 Provider

如果你用 SQL Server,常见包是:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

如果你用 MySQL 或 PostgreSQL,则需要替换为对应 Provider。

3.2 配置连接串

appsettings.json 示例:

{
  "ConnectionStrings": {
    "Default": "Server=.;Database=DemoDb;Trusted_Connection=True;TrustServerCertificate=True;"
  }
}

3.3 注册 DbContext

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("Default")));

这一步的意义是:把数据库上下文注册进依赖注入容器,这样控制器、服务层后续就可以通过构造器注入使用它。


第四章 最基础的增删改查

4.1 查询

查询全部

var users = await dbContext.Users.ToListAsync();

按条件查单条

var user = await dbContext.Users.FirstOrDefaultAsync(x => x.Id == 1);

按主键查找

var user = await dbContext.Users.FindAsync(1);

4.2 新增

var user = new User
{
    Name = "Tom",
    Email = "tom@example.com",
    CreatedAt = DateTime.UtcNow
};

dbContext.Users.Add(user);
await dbContext.SaveChangesAsync();

4.3 更新

var user = await dbContext.Users.FindAsync(1);

if (user != null)
{
    user.Name = "Tommy";
    await dbContext.SaveChangesAsync();
}

4.4 删除

var user = await dbContext.Users.FindAsync(1);

if (user != null)
{
    dbContext.Users.Remove(user);
    await dbContext.SaveChangesAsync();
}

4.5 为什么一定要记住 SaveChangesAsync

很多初学者会写了 AddRemove、修改属性之后忘记调用 SaveChangesAsync(),结果代码看起来改了,数据库却没有变化。

你可以把它理解成“真正提交变更”的动作。


第五章 实体状态跟踪到底在做什么

EF Core 的一个核心机制是变更跟踪。

当你通过上下文查询出实体后,DbContext 会跟踪它:

  • 原始值是什么
  • 当前值变成了什么
  • 它现在是新增、修改还是删除状态

这样在调用 SaveChangesAsync() 时,框架才能自动生成对应的 INSERTUPDATEDELETE

5.1 常见状态

  • Added
  • Modified
  • Deleted
  • Unchanged
  • Detached

5.2 为什么这个机制重要

因为它直接决定:

  • 为什么只改属性也能更新数据库
  • 为什么跨上下文对象更新时要小心
  • 为什么列表查询很多时会有内存和性能成本

第六章 迁移机制非常重要

很多人把 EF Core 只当成 CRUD 工具,但它非常实用的一点其实是迁移。

迁移机制用于管理数据库结构变化,也就是:

  • 新增表
  • 增加字段
  • 修改字段约束
  • 新增索引

6.1 常见命令

dotnet ef migrations add InitCreate
dotnet ef database update

这两步通常表示:

  1. 根据当前实体模型生成迁移文件
  2. 把迁移应用到数据库

6.2 一个基本认知

迁移不是“魔法自动同步数据库”,而是“基于当前模型生成结构变更脚本并纳入版本管理”。

6.3 为什么迁移很重要

因为真实项目开发中,数据库结构会不断演进。如果没有迁移机制,团队协作和环境同步会非常混乱。


第七章 实体映射:数据注解与 Fluent API

实体和表之间的映射通常有两种主流方式。

7.1 数据注解

using System.ComponentModel.DataAnnotations;

public class User
{
    public int Id { get; set; }

    [MaxLength(50)]
    public string Name { get; set; } = string.Empty;

    [MaxLength(100)]
    public string Email { get; set; } = string.Empty;
}

数据注解适合简单场景,写法直接。

7.2 Fluent API

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<User>(entity =>
    {
        entity.ToTable("users");
        entity.HasKey(x => x.Id);
        entity.Property(x => x.Name).HasMaxLength(50).IsRequired();
        entity.Property(x => x.Email).HasMaxLength(100);
    });
}

Fluent API 更适合:

  • 复杂映射
  • 团队统一配置风格
  • 关系配置
  • 索引和约束配置

7.3 什么时候更推荐 Fluent API

中大型项目里,通常更推荐把复杂映射放在 Fluent API 中集中管理,因为:

  • 结构更清晰
  • 不污染实体类
  • 更适合复杂关系配置

第八章 关系映射:一对多、导航属性与 Include

真实业务里,很少只有单表。

8.1 一对多示例

public class Order
{
    public int Id { get; set; }
    public string OrderNo { get; set; } = string.Empty;
    public List<OrderItem> Items { get; set; } = new();
}

public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public Order? Order { get; set; }
}

8.2 查询关联数据

var order = await dbContext.Orders
    .Include(x => x.Items)
    .FirstOrDefaultAsync(x => x.Id == 1);

Include 的作用是加载导航属性对应的关联数据。

8.3 不要无脑 Include

这是非常常见的误区。

错误思路:

  • 列表接口默认把所有关联都带上

这样很容易导致:

  • SQL 变复杂
  • 返回数据过大
  • 查询性能下降

正确思路是:只加载当前场景真正需要的数据。


第九章 查询时最容易忽视的性能问题

EF Core 很方便,但方便不代表可以忽视性能。

9.1 只查需要的字段

错误示例:

var users = await dbContext.Users.ToListAsync();

如果你只需要列表中的 IdName,更推荐:

var users = await dbContext.Users
    .Select(x => new
    {
        x.Id,
        x.Name
    })
    .ToListAsync();

9.2 读多写少场景使用 AsNoTracking

var users = await dbContext.Users
    .AsNoTracking()
    .ToListAsync();

适合:

  • 列表查询
  • 只读详情页
  • 不需要更新实体的查询

9.3 注意 N+1 查询问题

如果你在循环里频繁基于导航或额外查询拿关联数据,就可能造成 N+1 问题。

9.4 不要把 ORM 当万能性能优化工具

当查询极其复杂、报表统计很多、需要对 SQL 做细粒度控制时,可以考虑:

  • 手写 SQL
  • 使用 FromSql
  • 结合 Dapper

第十章 更新数据时要注意什么

10.1 先查后改是最常见写法

var user = await dbContext.Users.FindAsync(id);
if (user == null)
{
    return;
}

user.Name = input.Name;
user.Email = input.Email;
await dbContext.SaveChangesAsync();

10.2 不要随手整对象覆盖

在真实业务里,直接把前端传来的对象整包覆盖到实体上,可能会带来:

  • 不该改的字段被改掉
  • 并发数据被覆盖
  • 审计字段丢失

更稳妥的方式是按字段明确赋值。

10.3 批量更新和删除要特别谨慎

如果批量数据量很大,要关注:

  • SQL 执行效率
  • 事务大小
  • 锁范围
  • 日志量

第十一章 事务与一致性

在很多简单场景下,一次 SaveChangesAsync() 就自带事务语义。但如果你的业务涉及多个步骤,仍然要有事务意识。

11.1 典型场景

  • 扣库存
  • 写订单
  • 记录日志
  • 更新余额

这些步骤如果必须一起成功,就要考虑事务边界。

11.2 一个简单思路

事务的重点不是“我会不会写 BeginTransaction”,而是先搞清楚:

  • 哪些操作必须一起成功
  • 失败后是否允许部分成功
  • 有没有并发写冲突

第十二章 EF Core 适合什么场景,不适合什么场景

12.1 适合

  • 以业务 CRUD 为主的系统
  • 模型结构相对清晰的后台项目
  • 希望统一管理实体和迁移的团队

12.2 不适合单独依赖它解决所有问题

以下场景要更谨慎:

  • 极其复杂的报表查询
  • 大量数据库特性深度优化
  • 需要强控制 SQL 细节的高性能场景

这时候可以考虑与 Dapper 混合使用。


第十三章 初学者最容易踩的坑

13.1 忘记 SaveChangesAsync

代码改了,但数据库没变。

13.2 无脑 Include

导致 SQL 复杂度和返回数据量暴涨。

13.3 查询列表不加 AsNoTracking

在只读场景下徒增跟踪成本。

13.4 把前端对象直接映射回实体整包更新

容易覆盖掉不该改的字段。

13.5 把 EF Core 当成“不需要理解数据库”的理由

这会让你在建模、索引、事务、并发和性能问题上吃很多亏。


第十四章 入门阶段建议掌握到什么程度

如果你刚开始学 EF Core,建议至少做到:

  1. 能定义实体和 DbContext
  2. 能完成基础增删改查
  3. 能使用迁移管理数据库结构
  4. 知道 Include、投影查询、AsNoTracking 的区别
  5. 理解“查询”和“保存变更”是两类不同动作
  6. 对跟踪、关系加载和查询性能有基础意识

做到这一步,你就已经具备了把 ASP.NET Core 和数据库真正打通的能力。


下一步阅读


本文小结

EF Core 的价值不只是“写得少”,而是让你用更自然的方式把对象模型、查询表达式和数据库访问串起来。但真正写业务时,仍然必须关注三件事:

  • 建模是否合理
  • 查询是否高效
  • 数据结构变更是否可管理

把这三块同时掌握,EF Core 才会成为你的开发加速器,而不是后期问题制造机。