Skip to content

Entity Framework 开发与疑难排障指南 ​

Entity Framework (EF) 是微软推荐的 .NET ORM 框架。本文档汇总了从基础概念到高级映射的实践经验,并针对常见的开发报错提供了深度排障建议。

1. 核心概念与开发模式 ​

ORM (Object-Relational Mapping) 实现了对象模型与关系型数据库之间的映射。EF 允许开发者以操作对象的方式来操作数据库,其底层依然是基于 ADO.NET 构建的。

1.1 三种开发模式深度解析 ​

  • Database First (数据库优先):
    • 原理:基于已有的数据库表结构,通过 VS 插件生成 .edmx 映射文件及实体类。
    • 痛点:当数据库字段类型变更或主键缺失时,映射文件极易报错(如 Error 6013)。
  • 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 提供了三种加载策略:

  1. 预加载 (Eager Loading):使用 .Include() 显式联表。适用于明确知道需要关联数据的情况。

    csharp
    var orders = context.Customers.Include(c => c.Orders).ToList();
  2. 延迟加载 (Lazy Loading):访问属性时才去查库。需满足:属性标记为 virtual、Context 未销毁、已启用 LazyLoading。注意:在循环中使用会导致 N+1 问题。

  3. 显式加载 (Explicit Loading):手动加载特定实体的关联项。

    csharp
    context.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] 特性。

  • 捕获异常:

    csharp
    try {
        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 就无法确定对象的唯一性。 解决:

  1. 数据库层修复:为表添加物理主键(Primary Key)。
  2. 逻辑主键识别:如果无法添加物理主键,确保表中有一列或几列是 NOT NULL 且唯一的,EF 有时会自动推断,否则需在 EDMX XML 中手动标注。

4.2 成员映射无效:字段类型不兼容 ​

错误示例:类型 excellentModel.User 中的成员 TotalCount 类型为 Edm.String 与数据库中的 MySql.int 不兼容。 解决流程:

  1. 核对表视图:双击 .edmx 文件,查看底部的“映射详细信息”。
  2. 强制同步:在数据库修改字段类型后,必须在 EF 视图中右键“从数据库更新模型” -> “刷新”选项卡 -> 选中表。
  3. 命名陷阱:刷新后,EF 可能会生成类似 TotalCount1 这样带后缀的属性,需手动在设计器中重命名回 TotalCount。

5. 性能与最佳实践 ​

5.1 查询性能优化 ​

  • IQueryable vs IEnumerable:始终优先使用 IQueryable 进行过滤,确保 SQL 在数据库端执行,而不是把所有数据拉到内存后再过滤。

  • 分页实践:

    csharp
    var 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 };

6. 参考资料 ​

方舟 · Ark