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

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

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


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

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

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

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

这样在调用 SaveChangesAsync() 时,框架才能自动生成对应的 INSERT、UPDATE、DELETE。

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();

如果你只需要列表中的 Id 和 Name,更推荐:

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 才会成为你的开发加速器,而不是后期问题制造机。