目录
-
(图片来源网络,侵删)- 什么是 Entity Framework (EF)?
- EF6 的工作模式:Database-First 和 Code-First
- 准备工作:安装 EF6 和创建项目
- 第一个 EF6 程序:使用 Code-First 创建数据模型
-
- DbContext:EF 的心脏
- DbSet:数据的集合
- CRUD 操作:增删改查
- LINQ to Entities:查询数据
- 跟踪与不跟踪:
AsNoTracking() - 事务处理
-
- 数据关系:一对一、一对多、多对多
- 数据迁移
- 数据注解与 Fluent API
- 存储过程与函数
- 并发处理
-
- 依赖注入
- 性能优化:延迟加载 vs. 预加载 vs. 显式加载
- 异步编程
- 避免 N+1 查询问题
-
(图片来源网络,侵删)推荐资源
第一部分:EF6 基础入门
什么是 Entity Framework (EF)?
Entity Framework (EF) 是微软官方提供的 对象关系映射 框架,它允许你使用 .NET 对象(C# 类)来与数据库进行交互,而无需编写大量的 SQL 语句。
核心思想:
- 领域模型:用 C# 类(实体/Entity)来表示你的业务数据和关系。
- 上下文:
DbContext类负责跟踪这些实体的变化,并与数据库进行同步。 - 数据库交互:EF 会自动将你对 C# 对象的操作(如添加、修改、删除)转换为相应的 SQL 命令,发送给数据库执行。
主要优势:

- 提高开发效率:无需手动编写和管理 SQL。
- 数据库无关性:可以轻松切换数据库(如从 SQL Server 切换到 SQLite 或 MySQL),只需修改配置即可。
- 面向对象:使用熟悉的面向对象思维进行数据操作。
EF6 的工作模式:Database-First 和 Code-First
EF6 主要支持两种开发模式:
-
Code-First (代码优先):(现代主流方式)
- 流程:先编写 C# 实体类,EF 根据这些类自动创建数据库表结构。
- 优点:完全以代码为中心,易于版本控制和测试,符合现代软件开发流程。
- 本教程将重点讲解 Code-First 模式。
-
Database-First (数据库优先)
- 流程:先有一个已存在的数据库,EF 工具根据数据库结构生成 C# 实体类和
DbContext。 - 优点:适用于维护已有数据库的项目。
- 缺点:数据库结构变更时,需要重新生成代码,可能覆盖手动修改。
- 流程:先有一个已存在的数据库,EF 工具根据数据库结构生成 C# 实体类和
准备工作:安装 EF6 和创建项目
-
创建项目:使用 Visual Studio 创建一个新的 控制台应用 (.NET Framework) 或 ASP.NET Web 应用。
-
安装 EF6 NuGet 包: 在解决方案资源管理器中,右键点击项目 -> “管理 NuGet 程序包”,搜索并安装以下两个核心包:
EntityFramework:包含了 EF6 的核心运行时库。EntityFramework.SqlServer:用于与 SQL Server 数据库交互的提供程序,如果你使用其他数据库(如 SQLite),需要安装对应的提供程序(如EntityFramework.SQLite)。
第一个 EF6 程序:使用 Code-First 创建数据模型
让我们创建一个简单的博客应用模型。
步骤 1:定义实体类
在项目中创建一个新的类文件,Models.cs。
// Models.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
// 博客文章
public class Post
{
public int PostId { get; set; }
[Required]
[StringLength(200)]
public string Title { get; set; }
[Required]
public string Content { get; set; }
public int BlogId { get; set; } // 外键
// 导航属性:指向所属的 Blog
public Blog Blog { get; set; }
}
// 博客
public class Blog
{
public int BlogId { get; set; }
[Required]
[StringLength(100)]
public string Name { get; set; }
// 导航属性:指向该博客的所有文章
public virtual ICollection<Post> Posts { get; set; }
}
public int PostId { get; set; }:EF 默认会将名为Id或类名Id的属性作为主键。[Required],[StringLength]:数据注解,用于验证和影响数据库生成。public virtual ICollection<Post> Posts { get; set; }:导航属性,表示一个Blog可以包含多个Post。virtual关键字 enables 延迟加载。
步骤 2:创建 DbContext 类
再创建一个类文件,BlogDbContext.cs。
// BlogDbContext.cs
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
// 继承自 DbContext
public class BlogDbContext : DbContext
{
// 构造函数,传入数据库连接字符串的名称
public BlogDbContext() : base("name=MyBlogConnectionString") { }
// 将实体类暴露为 DbSets
public DbSet<Blog> Blogs { get; set; }
public DbSet<Post> Posts { get; set; }
// 重写 OnModelCreating 来自定义模型行为
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
// 移除表名中复数的约定(Blogs 表会变成 Blog 表)
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}
}
步骤 3:配置连接字符串
打开 App.config 文件(对于控制台应用)或 Web.config 文件(对于 Web 应用),在 <configuration> 节点下的 <connectionStrings> 节点中添加连接字符串。
<connectionStrings>
<!-- name 必须与 DbContext 构造函数中的 "name=..." 参数匹配 -->
<add name="MyBlogConnectionString"
connectionString="Data Source=(LocalDB)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Blog.mdf;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
- 这个配置会创建一个 SQL Server Express LocalDB 数据库文件。
步骤 4:初始化数据库并执行操作
在 Program.cs 中编写代码来使用 EF。
// Program.cs
using System;
using System.Linq;
class Program
{
static void Main(string[] args)
{
// 使用 using 语句确保 DbContext 被正确释放
using (var context = new BlogDbContext())
{
// 1. 创建并保存一个新博客
var blog = new Blog { Name = "我的第一个博客" };
context.Blogs.Add(blog);
context.SaveChanges(); // 数据库和 Blog 表会被自动创建
Console.WriteLine("博客创建成功!ID: {0}", blog.BlogId);
// 2. 创建并保存一篇新文章
var post = new Post
{
Title = "EF6 教程第一部分",
Content = "这是关于 EF6 基础的内容。",
BlogId = blog.BlogId // 关联到刚创建的博客
};
context.Posts.Add(post);
context.SaveChanges();
Console.WriteLine("文章创建成功!ID: {0}", post.PostId);
// 3. 查询数据
var myBlog = context.Blogs.FirstOrDefault(b => b.Name == "我的第一个博客");
if (myBlog != null)
{
Console.WriteLine("\n查询博客: {0}", myBlog.Name);
foreach (var p in myBlog.Posts)
{
Console.WriteLine(" - 文章: {0}", p.Title);
}
}
}
Console.ReadKey();
}
}
运行程序:
当你第一次运行 Main 方法中的 context.SaveChanges() 时,EF 会发现数据库不存在,然后根据你的模型自动创建它,运行后,你会在项目的 bin\Debug 目录下看到一个 Blog.mdf 文件,这就是你的数据库。
第二部分:核心概念与操作
DbContext:EF 的心脏
DbContext 是 EF6 的核心类,它负责:
- 定义数据集(
DbSet<T>属性)。 - 跟踪实体对象的状态(新增、修改、删除)。
- 将实体状态的变化同步到数据库。
- 管理数据库连接。
DbSet:数据的集合
DbSet<T> 代表数据库中的一个表,你可以对它进行 LINQ 查询和修改操作。
var context = new BlogDbContext();
var allPosts = context.Posts; // 代表 Posts 表
var specificPost = context.Posts.Find(1); // 根据主键查找
var postsByTitle = context.Posts.Where(p => p.Title.Contains("EF6")); // LINQ 查询
CRUD 操作:增删改查
-
Create (创建):
var newPost = new Post { Title = "新文章", Content = "内容" }; context.Posts.Add(newPost); // 状态变为 Added context.SaveChanges(); // 执行 INSERT -
Read (读取):
// 根据ID查询 var post = context.Posts.Find(postId); // LINQ 查询 var posts = context.Posts.Where(p => p.BlogId == blogId).ToList();
-
Update (更新):
var postToUpdate = context.Posts.Find(postId); postToUpdate.Title = "修改后的标题"; // context.Entry(postToUpdate).State = EntityState.Modified; // 也可以手动设置状态 context.SaveChanges(); // 执行 UPDATE
-
Delete (删除):
var postToDelete = context.Posts.Find(postId); context.Posts.Remove(postToDelete); // 状态变为 Deleted context.SaveChanges(); // 执行 DELETE
LINQ to Entities:查询数据
EF6 将 LINQ 查询翻译成 SQL,支持大部分 LINQ 操作符。
// 简单查询
var posts = context.Posts.Where(p => p.Title.Length > 10).OrderBy(p => p.Title);
// 聚合查询
var postCount = context.Posts.Count();
var avgTitleLength = context.Posts.Average(p => p.Title.Length);
// 复杂查询(导航属性)
var blogs = context.Blogs
.Where(b => b.Posts.Any(p => p.Content.Contains("教程")))
.Select(b => new { b.Name, PostCount = b.Posts.Count() });
跟踪与不跟踪:AsNoTracking()
默认情况下,DbContext 会从数据库查询出的实体进行跟踪,这意味着如果你修改了这些实体,调用 SaveChanges() 时 EF 会自动生成 UPDATE 语句。
- 需要跟踪:当你查询出实体后,需要对其进行修改并保存时。
- 不需要跟踪:当你只是读取数据用于展示(如填充到 ViewModel 或 DTO),并且不需要保存回数据库时,使用
AsNoTracking()可以显著提高查询性能,并减少内存占用。
// 不进行跟踪,性能更高 var postsForDisplay = context.Posts.AsNoTracking().Where(p => p.BlogId == 1).ToList();
事务处理
默认情况下,每次调用 SaveChanges() 都会在一个单独的事务中执行,如果你需要在一个事务中执行多个操作(同时添加一个博客和一篇文章),你需要手动管理事务。
using (var transaction = context.Database.BeginTransaction())
{
try
{
var blog = new Blog { Name = "事务测试博客" };
context.Blogs.Add(blog);
context.SaveChanges(); // 事务的一部分
var post = new Post { Title = "事务测试文章", BlogId = blog.BlogId };
context.Posts.Add(post);
context.SaveChanges(); // 事务的另一部分
transaction.Commit(); // 所有操作成功,提交事务
}
catch (Exception)
{
transaction.Rollback(); // 任何操作失败,回滚事务
throw;
}
}
第三部分:进阶特性
数据关系:一对一、一对多、多对多
-
一对多:一个
Blog对应多个Post,通过在Post类中定义外键BlogId和导航属性Blog来实现。public class Post { // ... public int BlogId { get; set; } public Blog Blog { get; set; } } -
一对一:一个
User对应一个UserProfile,需要在主键端使用[Key]和[ForeignKey]。public class User { public int UserId { get; set; } public string Username { get; set; } public UserProfile Profile { get; set; } } public class UserProfile { [Key, ForeignKey("User")] public int UserId { get; set; } // 主键同时也是外键 public string Bio { get; set; } public User User { get; set; } } -
多对多:一个
Student可以选修多门Course,一门Course也可以被多个Student选修,需要一个“连接表”来表示关系。public class Student { public int StudentId { get; set; } public string Name { get; set; } public virtual ICollection<Course> Courses { get; set; } } public class Course { public int CourseId { get; set; } public string Title { get; set; } public virtual ICollection<Student> Students { get; set; } } // EF 会自动创建一个名为 Student_Course 的连接表。
数据迁移
当你修改了实体类(如添加新属性、新类、修改关系),数据库结构并不会自动更新。数据迁移 功能就是为了解决这个问题。
-
启用迁移: 在 程序包管理器控制台 (Package Manager Console) 中运行:
Enable-Migrations
这会在项目中创建一个
Migrations文件夹,并包含一个Configuration.cs文件。 -
添加迁移: 每当你修改了模型,运行以下命令来生成一个描述这些变更的迁移脚本。
Add-Migration "AddPostContentProperty"
这会在
Migrations文件夹中创建一个新的 C# 文件(如xxxxxxxx_AddPostContentProperty.cs),其中包含Up()(应用变更)和Down()(回滚变更)方法。 -
更新数据库: 运行以下命令将迁移应用到数据库。
Update-Database
这会执行
Up()方法,修改数据库结构,它还会在数据库中创建一个__MigrationHistory表来跟踪已应用的迁移。
数据注解与 Fluent API
用于配置模型的行为,定义列名、数据类型、关系等。
-
数据注解:直接在实体类的属性上使用特性。
public class Post { public int PostId { get; set; } [Column("post_title", TypeName = "nvarchar(200)")] [Required] public string Title { get; set; } } -
Fluent API:在
DbContext的OnModelCreating方法中进行集中配置,更强大、更灵活,可以配置一些数据注解无法实现的东西。protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<Post>() .Property(p => p.Title) .HasColumnName("post_title") .HasColumnType("nvarchar(200)") .IsRequired(); }优先级:Fluent API > 数据注解 > 默认约定。
存储过程与函数
EF6 可以调用数据库中的存储过程。
-
在数据库中创建存储过程:
CREATE PROCEDURE GetPostsByBlog @BlogId int AS BEGIN SELECT * FROM Posts WHERE BlogId = @BlogId; END -
在 DbContext 中映射:
public class BlogDbContext : DbContext { // ... public ObjectResult<Post> GetPostsByBlog(int blogId) { return this.Database.SqlQuery<Post>("GetPostsByBlog @BlogId", new SqlParameter("@BlogId", blogId)); } } -
调用:
var posts = context.GetPostsByBlog(1).ToList();
并发处理
当多个用户同时修改同一数据时,可能会发生并发冲突,EF6 使用乐观并发来处理。
- 实现方式:在实体中添加一个
[Timestamp]或[ConcurrencyCheck]属性。[Timestamp](推荐):用于rowversion/timestamp类型,数据库会自动在每次更新时修改它,EF 会检查这个值是否在读取后发生了变化。[ConcurrencyCheck]:用于常规类型,EF 会检查所有标记了此属性的列的值是否变化。
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
[Timestamp] // 乐观并发标记
public byte[] RowVersion { get; set; }
}
当发生并发冲突时,SaveChanges() 会抛出 DbUpdateConcurrencyException,你需要捕获这个异常并处理它(向用户显示“数据已被他人修改,请刷新后重试”)。
第四部分:最佳实践与性能优化
依赖注入
将 DbContext 的生命周期与 Web 请求或服务生命周期绑定,而不是在每次操作时都 new 一个,这能提高性能和可维护性。
在 ASP.NET 中,可以使用 System.Web.Http 或 Microsoft.Extensions.DependencyInjection 来注册 DbContext。
// 在 Startup.cs 中 (使用 Microsoft.Extensions.DependencyInjection)
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<BlogDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyBlogConnectionString")));
}
性能优化:加载模式
-
延迟加载:当第一次访问导航属性时,EF 才会从数据库加载相关数据。
- 优点:代码简单,按需加载。
- 缺点:可能导致 N+1 查询问题。
- 启用:在导航属性上使用
virtual关键字。
-
预加载:使用
Include()方法在初始查询时就加载相关数据。- 优点:避免 N+1 查询,一次性获取所有需要的数据。
- 缺点:可能加载不必要的数据,增加网络传输量。
var blog = context.Blogs.Include(b => b.Posts).FirstOrDefault(b => b.BlogId == 1);
-
显式加载:在代码中显式地请求加载相关数据。
- 优点:控制力强,不会自动加载。
- 缺点:代码稍显繁琐。
var blog = context.Blogs.Find(1); context.Entry(blog).Collection(b => b.Posts).Load(); // 加载 Posts 集合
异步编程
对于所有可能阻塞 I/O 操作(如数据库查询)的方法,都应该使用异步版本,以避免阻塞线程,提高应用吞吐量。
ToListAsync()代替ToList()FindAsync()代替Find()SaveChangesAsync()代替SaveChanges()
public async Task<List<Post>> GetPostsAsync(int blogId)
{
// 使用 AsNoTracking 提高性能
return await context.Posts
.AsNoTracking()
.Where(p => p.BlogId == blogId)
.ToListAsync();
}
避免 N+1 查询问题
问题场景:延迟加载下,循环查询 N 个实体,每个实体又触发一次查询关联数据,总共产生 N+1 次数据库查询。
// 错误示例 (延迟加载导致 N+1)
var blogs = context.Blogs.ToList();
foreach (var blog in blogs)
{
var posts = blog.Posts; // 每次循环都会触发一次数据库查询
}
解决方案:使用 预加载。
// 正确示例 (使用 Include 避免 N+1)
var blogs = context.Blogs.Include(b => b.Posts).ToList();
foreach (var blog in blogs)
{
var posts = blog.Posts; // 数据已在内存中,无需再次查询
}
第五部分:总结与资源
Entity Framework 6 是一个成熟、强大的 ORM 框架,极大地简化了 .NET 应用程序的数据访问层开发,通过 Code-First 模式,你可以专注于业务逻辑,而将数据库的创建和维护交给 EF 自动处理。
掌握 EF6 的关键在于:
- 理解
DbContext和DbSet的核心作用。 - 熟练掌握 CRUD 操作和 LINQ 查询。
- 学会使用数据迁移来管理数据库模型的演进。
- 了解并应用性能优化技巧,如
AsNoTracking()、正确的加载模式和异步编程。
推荐资源
- 官方文档:
- Entity Framework 6 - Documentation (这是最权威、最全面的资源)
- 教程与博客:
- Entity Framework Tutorial by Guru99 (英文,非常详细)
- MSDN Magazine - Data Points (经典的系列文章)
- 视频课程:
在 Pluralsight, Udemy, Coursera 等平台上有大量高质量的 EF6 课程。
- 社区与问答:
- Stack Overflow (遇到问题,先搜索这里)
希望这份详细的教程能帮助你顺利入门并精通 Entity Framework 6!
