目录
-
- [一、先搞懂:什么是 EFCore 模型映射?](#一、先搞懂:什么是 EFCore 模型映射?)
- [二、数据注解(Data Annotations):贴标签式映射](#二、数据注解(Data Annotations):贴标签式映射)
-
- [1. 核心注解(重点讲 [Table])](#1. 核心注解(重点讲 [Table]))
- [2. 代码示例(User 实体 + 数据注解)](#2. 代码示例(User 实体 + 数据注解))
- [3. 数据注解常踩的坑(新手必看)](#3. 数据注解常踩的坑(新手必看))
- [三、FluentAPI:灵活的 "手工定制" 映射](#三、FluentAPI:灵活的 “手工定制” 映射)
-
- [1. 核心 API(对应数据注解)](#1. 核心 API(对应数据注解))
- [2. 代码示例(用 FluentAPI 实现和上面一样的 User 映射)](#2. 代码示例(用 FluentAPI 实现和上面一样的 User 映射))
- [3. FluentAPI 常踩的坑](#3. FluentAPI 常踩的坑)
- [四、数据注解 VS FluentAPI:怎么选?](#四、数据注解 VS FluentAPI:怎么选?)
- [五、高频踩坑点汇总(90% 新手必踩)](#五、高频踩坑点汇总(90% 新手必踩))
-
- [1.表名 / Schema 错误导致表不存在](#1.表名 / Schema 错误导致表不存在)
- 2.列类型不匹配导致插入失败
- [3.主键未配置导致 EFCore 报错](#3.主键未配置导致 EFCore 报错)
- 4.可空性逻辑冲突
- [5. [NotMapped] 遗漏导致多余列](#5. [NotMapped] 遗漏导致多余列)
- 六、实战完整流程(从模型到数据库)
-
- 1.创建实体类(User.cs):参考第二部分的代码
- [2.创建 DbContext(AppDbContext.cs):参考第三部分的代码](#2.创建 DbContext(AppDbContext.cs):参考第三部分的代码)
- 3.配置连接字符串(appsettings.json):
- [4.添加迁移并更新数据库(Package Manager Console):](#4.添加迁移并更新数据库(Package Manager Console):)
- [5.测试 CRUD:](#5.测试 CRUD:)
- [七、互动交流:你的 EFCore 映射踩坑经历?](#七、互动交流:你的 EFCore 映射踩坑经历?)
- 总结
作为ASP.NET开发中最主流的 ORM 框架,Entity Framework Core(EFCore)的核心能力之一就是将 C# 实体模型(Model)与数据库表建立映射关系 。新手常卡在 "模型怎么对应到数据库" 这个环节 ------ 要么表名 / 列名对不上,要么字段类型不匹配,甚至连主键都配置错了。
这篇文章我会用「生活类比 + 实战代码 + 避坑指南」的方式,把 EFCore 最核心的两种映射方式(数据注解、FluentAPI)讲透,让你少走 90% 的弯路。

一、先搞懂:什么是 EFCore 模型映射?
小节:核心概念 ------ 把 "C# 类" 翻译成 "数据库表"
EFCore 本质是 "翻译官",而模型映射 就是给翻译官定 "翻译规则":告诉它 "C# 里的User类对应数据库里的T_Users表"、"UserName属性对应表的U_Name列"、"Age属性是 int 类型且不能为空"。
生活类比:
把 C# 实体类比作「快递单模板」,数据库表比作「快递仓库的登记册」。模型映射就是规定:"模板上的'收件人姓名'要填到登记册的'收件人'列里"、"模板上的'手机号'必须填,不能空"。
核心作用:
- 让 EFCore 知道如何根据 C# 类创建 / 更新数据库表结构(Code First)
- 让 EFCore 知道如何把数据库表数据转换成C# 对象(查询时)
二、数据注解(Data Annotations):贴标签式映射
小节:最简单的映射方式 ------ 给模型 "贴标签"
数据注解是通过给实体类 / 属性加[特性](比如[Table])来指定映射规则,优点是简单直观,缺点是灵活性差,适合简单场景。
1. 核心注解(重点讲 [Table])
| 注解 | 作用 | 类比 |
|---|---|---|
| [Table] | 指定实体对应的数据库表名 + Schema | 给快递单模板贴标签:"归属于 XX 仓库的 XX 登记册" |
| [Column] | 指定属性对应的列名 + 数据类型 | 给模板的 "手机号" 栏贴标签:"填到登记册的'联系电话'列" |
| [Key] | 指定主键 | 给模板的 "快递单号" 栏贴标签:"这是唯一标识" |
| [Required] | 指定列非空 | 给模板的 "收件地址" 栏贴标签:"必须填" |
| [NotMapped] | 排除属性,不生成列 | 给模板的 "备注" 栏贴标签:"不用填到登记册" |
2. 代码示例(User 实体 + 数据注解)
csharp
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
// 实体模型:用户
// [Table]参数1:表名;参数2:数据库Schema(默认dbo)
[Table("T_Users", Schema = "UserCenter")]
public class User
{
// 主键
[Key]
public int Id { get; set; }
// 列名指定为U_Name,类型为nvarchar(50),非空
[Column("U_Name", TypeName = "nvarchar(50)")]
[Required(ErrorMessage = "用户名不能为空")]
public string UserName { get; set; }
// 列名指定为U_Age,非空
[Column("U_Age")]
[Required]
public int Age { get; set; }
// 排除该属性,不生成列
[NotMapped]
public string TempRemark { get; set; }
}
3. 数据注解常踩的坑(新手必看)
| 坑点 | 原因 | 解决方案 |
|---|---|---|
| [Table]的 Schema 写错导致表创建失败 | 数据库中不存在指定的 Schema(比如 UserCenter) | 1. 先创建 Schema;2. 或省略 Schema 用默认 dbo |
| 表名 / 列名大小写不匹配(Linux/Mac 数据库) | SQL Server/PostgreSQL 区分大小写,注解里写的是 T_Users,代码查的是 t_users | 统一大小写,或配置 EFCore 忽略大小写(UseCaseInsensitiveCollation()) |
| [Required]和可空类型冲突 | 给public int? Age加[Required] | 要么去掉?,要么去掉[Required],保持逻辑一致 |
| 忘记加[Key]导致 EFCore 推断错误 | EFCore 默认找 Id / 实体名 + Id 的属性当主键,若没有则报错 | 显式加[Key]指定主键,不要依赖默认推断 |
三、FluentAPI:灵活的 "手工定制" 映射
小节:高级玩法 ------ 给模型 "量体裁衣"
FluentAPI 是在DbContext的OnModelCreating方法中通过代码配置映射规则,优点是灵活性极高 (能配置数据注解做不到的场景),缺点是代码分散在 DbContext 中,适合复杂场景。
生活类比:
数据注解是 "买现成的带标签的衣服",FluentAPI 是 "找裁缝量体裁衣"------ 不仅能改衣服的 "名字 / 尺码",还能调整 "版型 / 细节"(比如复合主键、表关系)。
1. 核心 API(对应数据注解)
| FluentAPI 方法 | 对应注解 | 作用 |
|---|---|---|
| ToTable("表名", "Schema") | [Table] | 指定表名 + Schema |
| HasColumnName("列名") | [Column] | 指定列名 |
| HasColumnType("类型") | [Column] | 指定列数据类型 |
| IsRequired() | [Required] | 设置列非空 |
| Ignore(x => x.属性名) | [NotMapped] | 排除属性 |
| HasKey(x => new {x.属性1, x.属性2}) | [Key] | 设置复合主键(注解做不到) |
2. 代码示例(用 FluentAPI 实现和上面一样的 User 映射)
✅ 第一步:创建 DbContext
csharp
using Microsoft.EntityFrameworkCore;
public class AppDbContext : DbContext
{
// 定义DbSet,对应数据库表
public DbSet<User> Users { get; set; }
// 配置数据库连接
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 替换成你的数据库连接字符串
string connStr = "Server=.;Database=EFCoreDemo;Trusted_Connection=True;TrustServerCertificate=True;";
optionsBuilder.UseSqlServer(connStr);
}
// 核心:FluentAPI配置映射规则
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置User实体的映射规则
modelBuilder.Entity<User>(entity =>
{
// 对应[Table]:指定表名+Schema
entity.ToTable("T_Users", "UserCenter");
// 对应[Key]:指定主键
entity.HasKey(x => x.Id);
// 配置UserName属性:对应[Column]+[Required]
entity.Property(x => x.UserName)
.HasColumnName("U_Name") // 列名
.HasColumnType("nvarchar(50)")// 列类型
.IsRequired(); // 非空
// 配置Age属性:对应[Column]+[Required]
entity.Property(x => x.Age)
.HasColumnName("U_Age")
.IsRequired();
// 对应[NotMapped]:排除TempRemark属性
entity.Ignore(x => x.TempRemark);
});
}
}
✅ 第二步:测试使用(Program.cs)
csharp
// 注册DbContext(ASP.NET Core中推荐这种方式,替代OnConfiguring)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<AppDbContext>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"));
});
var app = builder.Build();
// 测试查询
using (var scope = app.Services.CreateScope())
{
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var users = dbContext.Users.ToList();
Console.WriteLine($"查询到{users.Count}个用户");
}
app.Run();
3. FluentAPI 常踩的坑
| 坑点 | 原因 | 解决方案 |
|---|---|---|
| 忘记调用modelBuilder.Entity() | 配置代码没生效,EFCore 用默认规则 | 所有实体的 FluentAPI 配置都要包在modelBuilder.Entity<实体>()里 |
| 配置顺序导致规则被覆盖 | 先配置IsRequired(false),后配置IsRequired(true),最终生效的是后者 | 统一配置顺序,或避免重复配置同一属性 |
| 复合主键配置不全 | HasKey(x => new {x.Id, x.Code})少写一个属性 | 复合主键必须把所有字段都放进匿名对象里 |
| 导航属性映射错误 | 配置HasOne/WithMany时方向搞反 | 先理清 "一对多 / 多对多" 关系,再按 "主表→从表" 顺序配置 |
四、数据注解 VS FluentAPI:怎么选?
小节:选型指南 ------ 按场景选对方式
| 维度 | 数据注解 | FluentAPI |
|---|---|---|
| 灵活性 | 低(仅支持基础配置) | 高(支持复合主键、表关系等) |
| 代码位置 | 实体类中(就近) | DbContext 中(集中) |
| 维护性 | 简单场景易维护 | 复杂场景易维护 |
| 适用场景 | 简单实体、快速开发 | 复杂实体、团队规范、集中配置 |
映射方式选择流程图
是
否
是
否
是
否
开始
需求是简单映射?
用数据注解(快速)
需要复合主键/表关系?
用FluentAPI(灵活)
团队要求集中配置?
结束
关键规则: 如果同时使用两种方式,FluentAPI 的优先级更高(会覆盖数据注解的配置)。
五、高频踩坑点汇总(90% 新手必踩)
小节:避坑指南 ------ 少走弯路就是提效
1.表名 / Schema 错误导致表不存在
- 现象:查询时报 "无效的对象名 'T_Users'"
- 原因:[Table]写错表名,或 Schema 不存在
- 解决:核对表名 / Schema,或用迁移自动创建(Add-Migration Init)
2.列类型不匹配导致插入失败
- 现象:插入字符串时报 "字符串或二进制数据将被截断"
- 原因:HasColumnType("nvarchar(10)")但插入了 20 个字符的字符串
- 解决:根据业务调整列长度,或用HasMaxLength(50)替代硬编码类型
3.主键未配置导致 EFCore 报错
- 现象:启动时报 "无法为实体类型 'User' 找到主键"
- 原因:实体没有 Id / 实体名 + Id 属性,也没加[Key]/FluentAPI 配置
- 解决:显式用[Key]或HasKey指定主键
4.可空性逻辑冲突
- 现象:保存时报 "列 'U_Age' 不允许空值"
- 原因:public int? Age加了[Required](可空类型 + 非空 注解冲突)
- 解决:要么去掉?,要么去掉[Required]
5. [NotMapped] 遗漏导致多余列
- 现象:数据库表多出TempRemark列
- 原因:忘记给临时属性加[NotMapped]或Ignore
- 解决:给不需要映射的属性加排除配置
六、实战完整流程(从模型到数据库)
小节:手把手教你落地 ------ 从代码到数据库
1.创建实体类(User.cs):参考第二部分的代码
2.创建 DbContext(AppDbContext.cs):参考第三部分的代码
3.配置连接字符串(appsettings.json):
json
{
"ConnectionStrings": {
"DefaultConnection": "Server=.;Database=EFCoreDemo;Trusted_Connection=True;TrustServerCertificate=True;"
}
}
4.添加迁移并更新数据库(Package Manager Console):
bash
# 添加迁移(记录模型变更)
Add-Migration InitCreateUserTable
# 更新数据库(执行迁移,创建表)
Update-Database
5.测试 CRUD:
csharp
// 新增用户
var newUser = new User { UserName = "张三", Age = 25 };
dbContext.Users.Add(newUser);
await dbContext.SaveChangesAsync();
// 查询用户
var user = await dbContext.Users.FirstOrDefaultAsync(x => x.UserName == "张三");
Console.WriteLine($"用户名:{user.U_Name},年龄:{user.U_Age}");
七、互动交流:你的 EFCore 映射踩坑经历?
学技术的路上,踩坑是常态 ------ 你在使用 EFCore 模型映射时,遇到过哪些印象深刻的坑?或者你更偏爱哪种映射方式?
欢迎在评论区分享你的踩坑经历、解决方案,或者对 EFCore 模型映射的疑问,我会逐一回复~
总结
✅1.EFCore 模型映射是 "C# 实体↔数据库表" 的翻译规则,核心有数据注解(简单)和 FluentAPI(灵活)两种方式;
✅2.[Table]用于指定实体对应的表名 + Schema,FluentAPI 对应ToTable(),后者优先级更高;
✅3.新手最易踩的坑是表名 / 列名不匹配、主键配置错误、可空性冲突,需重点核对配置逻辑。