引言
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 提供了三种主要的开发模式:
- Database First(数据库优先):先设计数据库,然后通过 EF 工具生成实体类和上下文。
- Model First(模型优先):先在 EF 设计器中创建实体模型,然后生成数据库。
- 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 迁移团队协作
在团队开发中,迁移文件需要被版本控制。当多个开发者同时修改数据库时,可能会遇到冲突。解决冲突的方法是:
- 拉取最新的迁移文件。
- 如果有冲突,手动解决迁移文件中的冲突。
- 运行
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 数据库迁移失败
迁移失败时,可以检查以下几点:
- 确保数据库连接字符串正确。
- 检查迁移文件是否有语法错误。
- 如果迁移已应用到数据库,但代码中未反映,可以尝试回滚迁移。
10. 总结
Entity Framework 是一个功能强大的 ORM 框架,通过本指南的学习,你应该已经掌握了从基础到高级的 EF 使用技巧。记住,实践是学习的关键,建议你通过实际项目来巩固所学知识。随着 EF 的不断更新,保持对新特性的关注,将帮助你更高效地开发应用程序。
附录:参考资源
通过本指南的学习,你将能够熟练使用 Entity Framework 进行数据库操作,掌握核心技巧与实战经验,从而在实际项目中游刃有余。
