引言

Entity Framework (EF) 是微软推出的一个对象关系映射(ORM)框架,它允许开发者使用 .NET 对象来操作数据库,而无需编写大量的 SQL 代码。EF 支持多种数据库,包括 SQL Server、SQLite、MySQL 等,极大地简化了数据访问层的开发。本指南将从基础概念讲起,逐步深入到高级技巧和实战经验,帮助你全面掌握 EF 的核心功能。

1. Entity Framework 基础概念

1.1 什么是 Entity Framework?

Entity Framework 是一个开源的对象关系映射(ORM)框架,它将数据库中的表映射为 .NET 中的类(实体),将表中的列映射为类的属性。通过 EF,你可以使用 LINQ(Language Integrated Query)来查询数据库,而 EF 会将这些查询转换为 SQL 语句并执行。

1.2 EF 的三种开发模式

EF 提供了三种主要的开发模式:

  1. Database First(数据库优先):先设计数据库,然后通过 EF 工具生成实体类和上下文。
  2. Model First(模型优先):先在 EF 设计器中创建实体模型,然后生成数据库。
  3. Code First(代码优先):先编写实体类和上下文,然后通过 EF 生成数据库。

目前,Code First 是最流行的模式,因为它允许开发者完全用代码控制数据库结构。

1.3 核心组件

  • DbContext:表示与数据库的会话,用于查询和保存数据。
  • DbSet:表示数据库中的表,用于执行 CRUD 操作。
  • 实体(Entity):映射到数据库表的类。
  • LINQ to Entities:用于查询数据库的 LINQ 查询。

2. 环境搭建与项目创建

2.1 安装 EF

在 .NET 项目中,你可以通过 NuGet 包管理器安装 EF。以 .NET Core 为例,打开终端并运行以下命令:

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

如果你使用的是 SQL Server,还需要安装对应的数据库提供程序。对于其他数据库,如 SQLite,可以安装 Microsoft.EntityFrameworkCore.Sqlite

2.2 创建实体类

假设我们要创建一个简单的博客系统,包含文章(Article)和作者(Author)两个实体。

// Author.cs
public class Author
{
    public int AuthorId { get; set; }
    public string Name { get; set; }
    public string Email { get; set; }
    public List<Article> Articles { get; set; }
}

// Article.cs
public class Article
{
    public int ArticleId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedDate { get; set; }
    public int AuthorId { get; set; }
    public Author Author { get; set; }
}

2.3 创建 DbContext

创建一个继承自 DbContext 的类,用于管理实体和数据库连接。

using Microsoft.EntityFrameworkCore;

public class BlogContext : DbContext
{
    public DbSet<Author> Authors { get; set; }
    public DbSet<Article> Articles { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        // 配置数据库连接字符串
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BlogDb;Trusted_Connection=True;");
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        // 配置实体关系
        modelBuilder.Entity<Article>()
            .HasOne(a => a.Author)
            .WithMany(a => a.Articles)
            .HasForeignKey(a => a.AuthorId);
    }
}

2.4 生成数据库

在 .NET Core 项目中,你可以使用 EF Core 的迁移工具来生成数据库。首先,安装 EF Core 工具:

dotnet tool install --global dotnet-ef

然后,在项目目录中运行以下命令来创建迁移:

dotnet ef migrations add InitialCreate
dotnet ef database update

这将创建一个名为 InitialCreate 的迁移,并将数据库更新到最新状态。

3. 基本 CRUD 操作

3.1 创建(Create)

using (var context = new BlogContext())
{
    var author = new Author
    {
        Name = "张三",
        Email = "zhangsan@example.com"
    };

    context.Authors.Add(author);
    context.SaveChanges();

    var article = new Article
    {
        Title = "Entity Framework 入门",
        Content = "这是一篇关于 Entity Framework 的入门文章。",
        PublishedDate = DateTime.Now,
        AuthorId = author.AuthorId
    };

    context.Articles.Add(article);
    context.SaveChanges();
}

3.2 读取(Read)

using (var context = new BlogContext())
{
    // 查询所有文章
    var allArticles = context.Articles.ToList();

    // 查询特定作者的文章
    var authorArticles = context.Articles
        .Where(a => a.Author.Name == "张三")
        .ToList();

    // 使用 LINQ 查询
    var recentArticles = context.Articles
        .Where(a => a.PublishedDate > DateTime.Now.AddDays(-7))
        .OrderByDescending(a => a.PublishedDate)
        .ToList();
}

3.3 更新(Update)

using (var context = new BlogContext())
{
    var article = context.Articles
        .FirstOrDefault(a => a.ArticleId == 1);

    if (article != null)
    {
        article.Title = "更新后的标题";
        article.Content = "更新后的内容";
        context.SaveChanges();
    }
}

3.4 删除(Delete)

using (var context = new BlogContext())
{
    var article = context.Articles
        .FirstOrDefault(a => a.ArticleId == 1);

    if (article != null)
    {
        context.Articles.Remove(article);
        context.SaveChanges();
    }
}

4. 高级查询技巧

4.1 使用 LINQ 查询

LINQ 是 EF 的强大功能,允许你使用类似 SQL 的语法查询数据。

using (var context = new BlogContext())
{
    // 分页查询
    var pageSize = 10;
    var pageIndex = 1;
    var articles = context.Articles
        .OrderBy(a => a.ArticleId)
        .Skip((pageIndex - 1) * pageSize)
        .Take(pageSize)
        .ToList();

    // 包含关联实体
    var articlesWithAuthors = context.Articles
        .Include(a => a.Author)
        .ToList();

    // 多条件查询
    var filteredArticles = context.Articles
        .Where(a => a.PublishedDate > DateTime.Now.AddDays(-30) && a.Title.Contains("Entity"))
        .OrderByDescending(a => a.PublishedDate)
        .ToList();
}

4.2 原始 SQL 查询

有时,你需要执行原始 SQL 查询以提高性能或处理复杂查询。

using (var context = new BlogContext())
{
    // 执行原始 SQL 查询
    var articles = context.Articles
        .FromSqlRaw("SELECT * FROM Articles WHERE PublishedDate > {0}", DateTime.Now.AddDays(-7))
        .ToList();

    // 执行存储过程
    var result = context.Set<Article>()
        .FromSqlRaw("EXEC GetRecentArticles @days = {0}", 7)
        .ToList();
}

4.3 异步查询

在现代应用中,异步操作可以提高应用程序的响应性。

using (var context = new BlogContext())
{
    // 异步查询
    var articles = await context.Articles
        .Where(a => a.PublishedDate > DateTime.Now.AddDays(-7))
        .ToListAsync();

    // 异步保存
    var article = new Article
    {
        Title = "异步文章",
        Content = "这是一篇异步保存的文章。",
        PublishedDate = DateTime.Now,
        AuthorId = 1
    };
    context.Articles.Add(article);
    await context.SaveChangesAsync();
}

5. 数据库迁移

5.1 迁移基础

迁移是 EF Core 的核心功能,允许你以代码方式管理数据库架构的变化。

# 添加迁移
dotnet ef migrations add AddArticleCategory

# 更新数据库
dotnet ef database update

# 回滚迁移
dotnet ef database update PreviousMigration

5.2 迁移脚本生成

你可以生成 SQL 脚本,用于在生产环境中应用迁移。

dotnet ef migrations script --idempotent

5.3 迁移团队协作

在团队开发中,迁移文件需要被版本控制。当多个开发者同时修改数据库时,可能会遇到冲突。解决冲突的方法是:

  1. 拉取最新的迁移文件。
  2. 如果有冲突,手动解决迁移文件中的冲突。
  3. 运行 dotnet ef database update 更新数据库。

6. 性能优化

6.1 避免 N+1 查询问题

N+1 查询问题是 ORM 中常见的性能问题。例如,查询所有文章及其作者时,如果使用循环查询,会导致多次数据库查询。

// 错误示例:N+1 查询
var articles = context.Articles.ToList();
foreach (var article in articles)
{
    // 每次循环都会查询数据库
    var author = context.Authors.Find(article.AuthorId);
}

// 正确示例:使用 Include
var articles = context.Articles
    .Include(a => a.Author)
    .ToList();

6.2 使用投影(Projection)

投影可以减少查询返回的数据量,提高性能。

var articleTitles = context.Articles
    .Select(a => new { a.ArticleId, a.Title })
    .ToList();

6.3 批量操作

EF Core 6.0 引入了批量操作,可以显著提高批量插入、更新和删除的性能。

// 批量插入
var newArticles = new List<Article>();
for (int i = 0; i < 1000; i++)
{
    newArticles.Add(new Article
    {
        Title = $"批量文章 {i}",
        Content = "批量插入的内容",
        PublishedDate = DateTime.Now,
        AuthorId = 1
    });
}
context.Articles.AddRange(newArticles);
context.SaveChanges();

// 批量更新(EF Core 6.0+)
context.Articles
    .Where(a => a.PublishedDate < DateTime.Now.AddYears(-1))
    .ExecuteUpdate(a => a.SetProperty(p => p.Title, p => p.Title + " (归档)"));

// 批量删除(EF Core 6.0+)
context.Articles
    .Where(a => a.PublishedDate < DateTime.Now.AddYears(-2))
    .ExecuteDelete();

7. 事务处理

7.1 显式事务

using (var context = new BlogContext())
{
    using (var transaction = context.Database.BeginTransaction())
    {
        try
        {
            // 操作1
            var author = new Author { Name = "李四", Email = "lisi@example.com" };
            context.Authors.Add(author);
            context.SaveChanges();

            // 操作2
            var article = new Article
            {
                Title = "事务文章",
                Content = "这是一篇事务文章。",
                PublishedDate = DateTime.Now,
                AuthorId = author.AuthorId
            };
            context.Articles.Add(article);
            context.SaveChanges();

            transaction.Commit();
        }
        catch (Exception)
        {
            transaction.Rollback();
            throw;
        }
    }
}

7.2 隐式事务

EF 默认在调用 SaveChanges() 时使用事务。如果多个 SaveChanges() 调用需要原子性,可以使用显式事务。

8. 实战经验与最佳实践

8.1 项目结构

在大型项目中,建议将实体、上下文和配置分离到不同的项目或文件夹中。

Project/
├── Models/
│   ├── Entities/
│   │   ├── Author.cs
│   │   └── Article.cs
│   └── Configurations/
│       ├── AuthorConfiguration.cs
│       └── ArticleConfiguration.cs
├── Data/
│   └── BlogContext.cs
└── Services/
    └── BlogService.cs

8.2 依赖注入

在 ASP.NET Core 项目中,使用依赖注入(DI)来管理 DbContext 的生命周期。

// 在 Startup.cs 或 Program.cs 中注册
services.AddDbContext<BlogContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

// 在控制器中使用
public class BlogController : ControllerBase
{
    private readonly BlogContext _context;

    public BlogController(BlogContext context)
    {
        _context = context;
    }

    // ...
}

8.3 错误处理与日志

在生产环境中,需要记录 EF 生成的 SQL 语句和错误信息。

// 配置日志
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BlogDb;Trusted_Connection=True;")
        .LogTo(Console.WriteLine, LogLevel.Information)
        .EnableSensitiveDataLogging(); // 仅在开发环境中使用
}

8.4 单元测试

使用内存数据库进行单元测试,避免依赖真实的数据库。

// 安装 Microsoft.EntityFrameworkCore.InMemory
dotnet add package Microsoft.EntityFrameworkCore.InMemory

// 测试代码
public class BlogServiceTests
{
    [Fact]
    public void TestAddArticle()
    {
        // 使用内存数据库
        var options = new DbContextOptionsBuilder<BlogContext>()
            .UseInMemoryDatabase(databaseName: "TestDatabase")
            .Options;

        using (var context = new BlogContext(options))
        {
            var service = new BlogService(context);
            service.AddArticle("测试标题", "测试内容", 1);

            var article = context.Articles.FirstOrDefault(a => a.Title == "测试标题");
            Assert.NotNull(article);
        }
    }
}

9. 常见问题与解决方案

9.1 连接字符串管理

在不同环境中(开发、测试、生产)使用不同的连接字符串。

// 在 appsettings.json 中配置
{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlogDb;Trusted_Connection=True;"
  }
}

// 在代码中读取
var connectionString = Configuration.GetConnectionString("DefaultConnection");

9.2 并发冲突处理

当多个用户同时修改同一数据时,可能会发生并发冲突。EF 提供了乐观并发控制。

// 在实体中添加并发令牌
public class Article
{
    public int ArticleId { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }
    public DateTime PublishedDate { get; set; }
    public int AuthorId { get; set; }
    [Timestamp] // 或使用 [ConcurrencyCheck]
    public byte[] RowVersion { get; set; }
}

// 在更新时处理并发冲突
try
{
    context.SaveChanges();
}
catch (DbUpdateConcurrencyException ex)
{
    // 处理并发冲突
    var entry = ex.Entries.Single();
    var databaseValues = entry.GetDatabaseValues();

    // 显示冲突信息,让用户决定如何处理
    // ...
}

9.3 数据库迁移失败

迁移失败时,可以检查以下几点:

  1. 确保数据库连接字符串正确。
  2. 检查迁移文件是否有语法错误。
  3. 如果迁移已应用到数据库,但代码中未反映,可以尝试回滚迁移。

10. 总结

Entity Framework 是一个功能强大的 ORM 框架,通过本指南的学习,你应该已经掌握了从基础到高级的 EF 使用技巧。记住,实践是学习的关键,建议你通过实际项目来巩固所学知识。随着 EF 的不断更新,保持对新特性的关注,将帮助你更高效地开发应用程序。

附录:参考资源

通过本指南的学习,你将能够熟练使用 Entity Framework 进行数据库操作,掌握核心技巧与实战经验,从而在实际项目中游刃有余。