Appearance
Entity Framework 开发与疑难排障指南
Entity Framework (EF) 是微软推荐的 .NET ORM 框架。本文档汇总了从基础概念到高级映射的实践经验,并针对常见的开发报错提供了深度排障建议。
1. 核心概念与开发模式
ORM (Object-Relational Mapping) 实现了对象模型与关系型数据库之间的映射。EF 允许开发者以操作对象的方式来操作数据库,其底层依然是基于 ADO.NET 构建的。
1.1 三种开发模式深度解析
- Database First (数据库优先):
- 原理:基于已有的数据库表结构,通过 VS 插件生成
.edmx映射文件及实体类。 - 痛点:当数据库字段类型变更或主键缺失时,映射文件极易报错(如 Error 6013)。
- 原理:基于已有的数据库表结构,通过 VS 插件生成
- Model First (模型优先):
- 原理:在可视化设计器中先画出实体关系图,再由工具生成 SQL 脚本同步到数据库。
- 现状:在 EF Core 中已不再作为主流支持,主要存在于旧版 EF 6 项目中。
- Code First (代码优先 - 推荐):
原理:开发者编写 C# 类,EF 根据类定义自动生成数据库结构。
关键配置:
csharp// 禁用默认的复数表名约定(避免生成的表名带 s) protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); }
2. 核心 CRUD 操作与 SQL 生成
2.1 插入操作与标识值获取
当 EF 执行 Add() 后调用 SaveChanges(),它会生成类似如下的 SQL 语句来确保获取到新插入的 ID:
sql
exec sp_executesql N'insert [dbo].[UserInfo]([UserName], [UserPass], [RegTime], [Email])
values (@0, @1, @2, @3)
select [ID]
from [dbo].[UserInfo]
where @@ROWCOUNT > 0 and [ID] = scope_identity()', ...关键知识点:SCOPE_IDENTITY() vs @@IDENTITY
- SCOPE_IDENTITY():返回当前执行范围内(同一个存储过程、触发器或批处理)生成的最后一个标识值。EF 默认使用此函数,安全性更高。
- @@IDENTITY:返回当前会话中任何表生成的最后一个标识值,不受作用域限制。如果表上有触发器在另一张表插入了数据,
@@IDENTITY可能会返回触发器插入的 ID,导致业务逻辑出错。
2.2 导航属性加载策略
导航属性是 ORM 的灵魂,它定义了实体间的逻辑连接(一对多、多对多)。EF 提供了三种加载策略:
预加载 (Eager Loading):使用
.Include()显式联表。适用于明确知道需要关联数据的情况。csharpvar orders = context.Customers.Include(c => c.Orders).ToList();延迟加载 (Lazy Loading):访问属性时才去查库。需满足:属性标记为
virtual、Context 未销毁、已启用 LazyLoading。注意:在循环中使用会导致 N+1 问题。显式加载 (Explicit Loading):手动加载特定实体的关联项。
csharpcontext.Entry(customer).Collection(c => c.Orders).Load();
2.3 常用查询示例
csharp
// 1. 获取某个具体客户的所有订单号
var orders = context.Customers.Where(c => c.Id == 1).SelectMany(c => c.Orders).Select(o => o.OrderNum).ToList();
// 2. 通过订单反查用户名
var userName = context.Orders.Where(o => o.OrderNum == "10001").Select(o => o.Customer.UserName).FirstOrDefault();
// 3. 级联删除示例
var customer = context.Customers.Include(c => c.Orders).FirstOrDefault(c => c.Id == 1);
context.Orders.RemoveRange(customer.Orders); // 先删从表(或配置级联删除)
context.Customers.Remove(customer); // 再删主表
context.SaveChanges();3. 映射实践与高级配置
3.1 EF Core 多对多映射 (v2.0 - v3.1)
在早期版本的 EF Core 中,多对多映射必须显式定义中间表实体。
实体定义:
csharp
public class Post {
public int PostId { get; set; }
public string Title { get; set; }
public List<PostTag> PostTags { get; set; }
}
public class Tag {
public int TagId { get; set; }
public string TagName { get; set; }
public List<PostTag> PostTags { get; set; }
}
// 显式中间表实体
public class PostTag {
public int PostId { get; set; }
public Post Post { get; set; }
public int TagId { get; set; }
public Tag Tag { get; set; }
}Fluent API 配置:
csharp
protected override void OnModelCreating(ModelBuilder modelBuilder) {
// 1. 配置复合主键
modelBuilder.Entity<PostTag>().HasKey(pt => new { pt.PostId, pt.TagId });
// 2. 配置多对一关系(两端)
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Post)
.WithMany(p => p.PostTags)
.HasForeignKey(pt => pt.PostId);
modelBuilder.Entity<PostTag>()
.HasOne(pt => pt.Tag)
.WithMany(t => t.PostTags)
.HasForeignKey(pt => pt.TagId);
}查询示例 (ThenInclude):
csharp
// 注意:ThenInclude 在某些 VS 版本中可能存在智能感知失效的 Bug,但编译可以通过
var queryPost = await context.Posts
.Include(p => p.PostTags)
.ThenInclude(pt => pt.Tag)
.FirstOrDefaultAsync();4. 并发控制与事务管理
4.1 并发冲突处理 (Concurrency)
EF 默认使用 乐观并发 (Optimistic Concurrency) 模式。
配置:在实体类属性上添加
[ConcurrencyCheck]或[Timestamp]特性。捕获异常:
csharptry { db.SaveChanges(); } catch (DbUpdateConcurrencyException ex) { // 冲突处理逻辑:数据库赢、客户端赢、或合并更改 ex.Entries.Single().Reload(); // 数据库赢 }
4.2 事务处理 (Transactions)
SaveChanges() 内部默认会开启一个事务。如果需要跨多个 Context 或包含其他数据库操作,可手动管理事务:
csharp
using (var transaction = context.Database.BeginTransaction()) {
try {
context.SaveChanges();
// 其他 ADO.NET 操作...
transaction.Commit();
} catch (Exception) {
transaction.Rollback();
}
}5. 常见报错与深度排障 (FAQ)
4.1 错误 6013:缺少主键映射
现象:使用 DataBaseFirst 时,表/视图未定义主键,已排除此表。 深度解析:EF 内部使用 EntityKey 来跟踪对象状态。没有主键,EF 就无法确定对象的唯一性。 解决:
- 数据库层修复:为表添加物理主键(Primary Key)。
- 逻辑主键识别:如果无法添加物理主键,确保表中有一列或几列是
NOT NULL且唯一的,EF 有时会自动推断,否则需在 EDMX XML 中手动标注。
4.2 成员映射无效:字段类型不兼容
错误示例:类型 excellentModel.User 中的成员 TotalCount 类型为 Edm.String 与数据库中的 MySql.int 不兼容。 解决流程:
- 核对表视图:双击
.edmx文件,查看底部的“映射详细信息”。 - 强制同步:在数据库修改字段类型后,必须在 EF 视图中右键“从数据库更新模型” -> “刷新”选项卡 -> 选中表。
- 命名陷阱:刷新后,EF 可能会生成类似
TotalCount1这样带后缀的属性,需手动在设计器中重命名回TotalCount。
5. 性能与最佳实践
5.1 查询性能优化
IQueryable vs IEnumerable:始终优先使用
IQueryable进行过滤,确保 SQL 在数据库端执行,而不是把所有数据拉到内存后再过滤。分页实践:
csharpvar pageData = context.Users .OrderBy(u => u.Id) .Skip((pageIndex - 1) * pageSize) .Take(pageSize) .ToList();禁用追踪 (AsNoTracking):对于只读报表页面,使用
.AsNoTracking()可提升约 30% 的查询性能。
5.2 复杂关联查询 (Join)
虽然导航属性很方便,但在某些复杂场景下(如左外连接),显式 Join 更加清晰:
csharp
var query = from u in context.Users
join o in context.Orders on u.Id equals o.UserId into temp
from t in temp.DefaultIfEmpty() // 左外连接
select new { u.UserName, OrderDate = t.CreateTime };