目录

  1. 第一部分:EF6 基础入门

    entityframework6教程
    (图片来源网络,侵删)
    • 什么是 Entity Framework (EF)?
    • EF6 的工作模式:Database-First 和 Code-First
    • 准备工作:安装 EF6 和创建项目
    • 第一个 EF6 程序:使用 Code-First 创建数据模型
  2. 第二部分:核心概念与操作

    • DbContext:EF 的心脏
    • DbSet:数据的集合
    • CRUD 操作:增删改查
    • LINQ to Entities:查询数据
    • 跟踪与不跟踪:AsNoTracking()
    • 事务处理
  3. 第三部分:进阶特性

    • 数据关系:一对一、一对多、多对多
    • 数据迁移
    • 数据注解与 Fluent API
    • 存储过程与函数
    • 并发处理
  4. 第四部分:最佳实践与性能优化

    • 依赖注入
    • 性能优化:延迟加载 vs. 预加载 vs. 显式加载
    • 异步编程
    • 避免 N+1 查询问题
  5. 第五部分:总结与资源

    entityframework6教程
    (图片来源网络,侵删)

    推荐资源


第一部分:EF6 基础入门

什么是 Entity Framework (EF)?

Entity Framework (EF) 是微软官方提供的 对象关系映射 框架,它允许你使用 .NET 对象(C# 类)来与数据库进行交互,而无需编写大量的 SQL 语句。

核心思想:

  • 领域模型:用 C# 类(实体/Entity)来表示你的业务数据和关系。
  • 上下文DbContext 类负责跟踪这些实体的变化,并与数据库进行同步。
  • 数据库交互:EF 会自动将你对 C# 对象的操作(如添加、修改、删除)转换为相应的 SQL 命令,发送给数据库执行。

主要优势:

entityframework6教程
(图片来源网络,侵删)
  • 提高开发效率:无需手动编写和管理 SQL。
  • 数据库无关性:可以轻松切换数据库(如从 SQL Server 切换到 SQLite 或 MySQL),只需修改配置即可。
  • 面向对象:使用熟悉的面向对象思维进行数据操作。

EF6 的工作模式:Database-First 和 Code-First

EF6 主要支持两种开发模式:

  • Code-First (代码优先)(现代主流方式)

    • 流程:先编写 C# 实体类,EF 根据这些类自动创建数据库表结构。
    • 优点:完全以代码为中心,易于版本控制和测试,符合现代软件开发流程。
    • 本教程将重点讲解 Code-First 模式。
  • Database-First (数据库优先)

    • 流程:先有一个已存在的数据库,EF 工具根据数据库结构生成 C# 实体类和 DbContext
    • 优点:适用于维护已有数据库的项目。
    • 缺点:数据库结构变更时,需要重新生成代码,可能覆盖手动修改。

准备工作:安装 EF6 和创建项目

  1. 创建项目:使用 Visual Studio 创建一个新的 控制台应用 (.NET Framework) 或 ASP.NET Web 应用

  2. 安装 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 可以包含多个 Postvirtual 关键字 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 的连接表。

数据迁移

当你修改了实体类(如添加新属性、新类、修改关系),数据库结构并不会自动更新。数据迁移 功能就是为了解决这个问题。

  1. 启用迁移: 在 程序包管理器控制台 (Package Manager Console) 中运行:

    Enable-Migrations

    这会在项目中创建一个 Migrations 文件夹,并包含一个 Configuration.cs 文件。

  2. 添加迁移: 每当你修改了模型,运行以下命令来生成一个描述这些变更的迁移脚本。

    Add-Migration "AddPostContentProperty" 

    这会在 Migrations 文件夹中创建一个新的 C# 文件(如 xxxxxxxx_AddPostContentProperty.cs),其中包含 Up()(应用变更)和 Down()(回滚变更)方法。

  3. 更新数据库: 运行以下命令将迁移应用到数据库。

    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:在 DbContextOnModelCreating 方法中进行集中配置,更强大、更灵活,可以配置一些数据注解无法实现的东西。

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Post>()
                     .Property(p => p.Title)
                     .HasColumnName("post_title")
                     .HasColumnType("nvarchar(200)")
                     .IsRequired();
    }

    优先级:Fluent API > 数据注解 > 默认约定。

存储过程与函数

EF6 可以调用数据库中的存储过程。

  1. 在数据库中创建存储过程

    CREATE PROCEDURE GetPostsByBlog
        @BlogId int
    AS
    BEGIN
        SELECT * FROM Posts WHERE BlogId = @BlogId;
    END
  2. 在 DbContext 中映射

    public class BlogDbContext : DbContext
    {
        // ...
        public ObjectResult<Post> GetPostsByBlog(int blogId)
        {
            return this.Database.SqlQuery<Post>("GetPostsByBlog @BlogId", new SqlParameter("@BlogId", blogId));
        }
    }
  3. 调用

    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.HttpMicrosoft.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 的关键在于:

  1. 理解 DbContextDbSet 的核心作用。
  2. 熟练掌握 CRUD 操作和 LINQ 查询。
  3. 学会使用数据迁移来管理数据库模型的演进。
  4. 了解并应用性能优化技巧,如 AsNoTracking()、正确的加载模式和异步编程。

推荐资源

希望这份详细的教程能帮助你顺利入门并精通 Entity Framework 6!