C# 零基础到精通教程 - 第十五章:数据库编程——Entity Framework Core
第十四章我们学习了异步编程,知道了如何让程序在执行耗时操作时保持响应。但我们的数据仍然存储在内存中,程序一关闭就丢了。上一章我们学习了文件 I/O,可以读写文件了,但文件方式管理大量关联数据很麻烦——比如要找出"所有购买了 iPhone 15 的用户",用文件操作要写很多代码。
这一章要学的 Entity Framework Core(简称 EF Core)是更专业的解决方案。它是 .NET 官方推荐的 ORM(对象关系映射)框架,让你用 C# 对象操作数据库,不用写 SQL 语句。
15.1 什么是 ORM 和 EF Core?
15.1.1 没有 ORM 的困境
csharp
// 传统方式:需要手动写 SQL 语句
// 插入数据
string sql = "INSERT INTO Students (Name, Age) VALUES ('张三', 20)";
// 执行命令...
// 查询数据
string query = "SELECT * FROM Students WHERE Age > 18";
// 读取结果集,手动映射到 C# 对象
List<Student> students = new List<Student>();
while (reader.Read())
{
Student s = new Student();
s.Name = reader.GetString("Name");
s.Age = reader.GetInt32("Age");
students.Add(s);
}
// 问题:代码冗长、容易出错、需要手写 SQL
15.1.2 有 ORM 的解决方案
csharp
// 使用 EF Core:完全用 C# 对象操作
// 插入数据
context.Students.Add(new Student { Name = "张三", Age = 20 });
context.SaveChanges();
// 查询数据
var adults = context.Students
.Where(s => s.Age > 18)
.ToList();
// 不需要写任何 SQL![citation:7]
15.1.3 什么是 ORM?
ORM = 对象关系映射。把数据库表映射成 C# 类,把表中的行映射成对象,把 SQL 操作变成 C# 方法调用。
text
┌─────────────────────────────────────────────────────────────┐ │ ORM 映射关系 │ ├─────────────────────────────────────────────────────────────┤ │ │ │ 数据库 ──映射──> C# │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 表 (Table) │ ──────────> │ 类 (Class) │ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 行 (Row) │ ──────────> │ 对象 (Object)│ │ │ └─────────────┘ └─────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ ┌─────────────┐ ┌─────────────┐ │ │ │ 列 (Column)│ ──────────> │ 属性 (Property)│ │ │ └─────────────┘ └─────────────┘ │ │ │ └─────────────────────────────────────────────────────────────┘
15.1.4 EF Core 的核心组件
text
Entity Framework Core ├── 实体类(Entity) → 对应数据库表 ├── DbContext → 数据库会话,管理连接和跟踪 ├── DbSet<T> → 对应数据库中的表,CRUD 操作入口 ├── 迁移(Migration) → 同步实体类和数据库结构 └── 提供程序(Provider) → 连接特定数据库(SQL Server、MySQL、SQLite 等)[citation:7]
15.1.5 为什么选择 EF Core?
| 特点 | 说明 |
|---|---|
| 跨平台 | 支持 Windows、Linux、macOS |
| 高性能 | 比 EF 6 快很多,支持批量操作 |
| LINQ 支持 | 用 C# 写查询,有智能提示和类型检查 |
| 迁移管理 | 用代码管理数据库版本,方便团队协作 |
| 数据库无关 | 换数据库只需改配置,代码不用改 |
15.2 环境搭建与第一个 EF Core 程序
15.2.1 创建项目
bash
# 创建一个控制台应用 dotnet new console -n EFCoreDemo cd EFCoreDemo
15.2.2 安装 NuGet 包
bash
# EF Core 核心包 dotnet add package Microsoft.EntityFrameworkCore # SQL Server 提供程序(如果使用 SQL Server) dotnet add package Microsoft.EntityFrameworkCore.SqlServer # SQLite 提供程序(如果使用 SQLite,轻量级,适合学习) dotnet add package Microsoft.EntityFrameworkCore.Sqlite # 工具包(用于迁移) dotnet add package Microsoft.EntityFrameworkCore.Tools
15.2.3 定义实体类(Model)
csharp
// 学生实体
public class Student
{
public int Id { get; set; } // 主键(EF 会识别)
public string Name { get; set; }
public int Age { get; set; }
public string Email { get; set; }
public DateTime EnrollmentDate { get; set; }
// 导航属性:一个学生有多个成绩(一对多关系)
public ICollection<Grade> Grades { get; set; } = new List<Grade>();
}
// 成绩实体
public class Grade
{
public int Id { get; set; }
public string CourseName { get; set; }
public double Score { get; set; }
// 外键
public int StudentId { get; set; }
// 导航属性:这个成绩属于哪个学生
public Student Student { get; set; }
}
15.2.4 创建 DbContext
csharp
using Microsoft.EntityFrameworkCore;
// DbContext 是 EF Core 的核心类
public class SchoolDbContext : DbContext
{
// DbSet 对应数据库中的表
public DbSet<Student> Students { get; set; }
public DbSet<Grade> Grades { get; set; }
// 配置数据库连接
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// 使用 SQLite(文件数据库,不需要安装服务器)
optionsBuilder.UseSqlite("Data Source=school.db");
// 如果使用 SQL Server:
// optionsBuilder.UseSqlServer("Server=localhost;Database=SchoolDb;Trusted_Connection=True;");
}
// 可选:使用 Fluent API 配置实体(比数据注解更灵活)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置 Student 表
modelBuilder.Entity<Student>(entity =>
{
entity.ToTable("Students");
entity.HasKey(s => s.Id);
entity.Property(s => s.Name).IsRequired().HasMaxLength(100);
entity.Property(s => s.Email).HasMaxLength(200);
// 配置一对多关系
entity.HasMany(s => s.Grades)
.WithOne(g => g.Student)
.HasForeignKey(g => g.StudentId)
.OnDelete(DeleteBehavior.Cascade); // 删除学生时自动删除成绩
});
// 配置 Grade 表
modelBuilder.Entity<Grade>(entity =>
{
entity.ToTable("Grades");
entity.Property(g => g.Score).HasColumnType("decimal(5,2)");
});
// 也可以使用数据注解方式,不需要写这里
}
}
15.2.5 数据库迁移
bash
# 1. 创建迁移(生成迁移文件) dotnet ef migrations add InitialCreate # 2. 应用迁移(创建数据库和表) dotnet ef database update
执行后,会生成:
-
school.db文件(SQLite 数据库) -
表结构:Students 表和 Grades 表,带外键关系
15.2.6 第一个 CRUD 程序
csharp
using Microsoft.EntityFrameworkCore;
class Program
{
static void Main()
{
using (var context = new SchoolDbContext())
{
// ========== 1. 新增数据(Create)==========
Console.WriteLine("=== 新增学生 ===");
var student = new Student
{
Name = "张三",
Age = 20,
Email = "zhangsan@example.com",
EnrollmentDate = DateTime.Now,
Grades = new List<Grade>
{
new Grade { CourseName = "数学", Score = 85.5 },
new Grade { CourseName = "英语", Score = 90.0 }
}
};
context.Students.Add(student);
context.SaveChanges(); // 提交到数据库
Console.WriteLine($"新增学生成功!ID: {student.Id}");
// ========== 2. 查询数据(Read)==========
Console.WriteLine("\n=== 所有学生 ===");
var students = context.Students
.Include(s => s.Grades) // 加载关联的成绩数据
.ToList();
foreach (var s in students)
{
Console.WriteLine($"学生:{s.Name},年龄:{s.Age}");
foreach (var g in s.Grades)
{
Console.WriteLine($" 成绩:{g.CourseName} - {g.Score}");
}
}
// ========== 3. 条件查询 ==========
Console.WriteLine("\n=== 成年学生(年龄 >= 18)===");
var adults = context.Students
.Where(s => s.Age >= 18)
.OrderBy(s => s.Name)
.ToList();
foreach (var s in adults)
{
Console.WriteLine($"{s.Name} - {s.Age}岁");
}
// ========== 4. 更新数据(Update)==========
Console.WriteLine("\n=== 更新学生 ===");
var studentToUpdate = context.Students.FirstOrDefault(s => s.Name == "张三");
if (studentToUpdate != null)
{
studentToUpdate.Age = 21;
context.SaveChanges();
Console.WriteLine($"已将 {studentToUpdate.Name} 的年龄改为 {studentToUpdate.Age}");
}
// ========== 5. 删除数据(Delete)==========
Console.WriteLine("\n=== 删除学生 ===");
var studentToDelete = context.Students.FirstOrDefault(s => s.Name == "李四");
if (studentToDelete != null)
{
context.Students.Remove(studentToDelete);
context.SaveChanges();
Console.WriteLine($"已删除学生:{studentToDelete.Name}");
}
}
}
}
15.3 实体关系配置
15.3.1 关系类型概述
EF Core 支持三种关系类型:
text
关系类型 ├── 一对一(1:1) → 如:用户 ↔ 身份证 ├── 一对多(1:N) → 如:学生 ↔ 成绩(最常见) └── 多对多(N:N) → 如:学生 ↔ 课程(一个学生选多门课,一门课有多个学生)
15.3.2 一对多关系配置
csharp
// 方式1:使用数据注解
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性:一个学生有多条成绩
public ICollection<Grade> Grades { get; set; }
}
public class Grade
{
public int Id { get; set; }
public double Score { get; set; }
// 外键
public int StudentId { get; set; }
// 导航属性:这个成绩属于哪个学生
[ForeignKey("StudentId")]
public Student Student { get; set; }
}
// 方式2:使用 Fluent API(在 OnModelCreating 中)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(s => s.Grades) // 学生有多条成绩
.WithOne(g => g.Student) // 成绩属于一个学生
.HasForeignKey(g => g.StudentId) // 外键是 Grade.StudentId
.OnDelete(DeleteBehavior.Cascade); // 级联删除
}
15.3.3 多对多关系配置
EF Core 5.0+ 支持直接配置多对多关系,不需要手动创建中间表:
csharp
// 学生实体
public class Student
{
public int Id { get; set; }
public string Name { get; set; }
// 导航属性:一个学生选修多门课程
public ICollection<Course> Courses { get; set; }
}
// 课程实体
public class Course
{
public int Id { get; set; }
public string Title { get; set; }
// 导航属性:一门课程有多个学生
public ICollection<Student> Students { get; set; }
}
// Fluent API 配置多对多(EF Core 5.0+)
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity(j => j.ToTable("StudentCourses")); // 自动创建中间表
}
// 查询时使用
var studentWithCourses = context.Students
.Include(s => s.Courses)
.FirstOrDefault(s => s.Id == 1);
15.3.4 一对一关系配置
csharp
// 用户实体
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public UserProfile Profile { get; set; }
}
// 用户档案实体
public class UserProfile
{
public int Id { get; set; }
public string Address { get; set; }
public string Phone { get; set; }
// 外键
public int UserId { get; set; }
public User User { get; set; }
}
// Fluent API 配置
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>()
.HasOne(u => u.Profile)
.WithOne(p => p.User)
.HasForeignKey<UserProfile>(p => p.UserId);
}
15.4 CRUD 操作深入
15.4.1 新增数据
csharp
using (var context = new SchoolDbContext())
{
// 方式1:单独添加一个实体
var student = new Student { Name = "王五", Age = 22 };
context.Students.Add(student);
context.SaveChanges();
// 方式2:批量添加
var students = new List<Student>
{
new Student { Name = "赵六", Age = 19 },
new Student { Name = "小明", Age = 20 }
};
context.Students.AddRange(students);
context.SaveChanges();
// 方式3:通过导航属性自动添加关联实体
var newStudent = new Student
{
Name = "小红",
Age = 18,
Grades = new List<Grade>
{
new Grade { CourseName = "语文", Score = 88 },
new Grade { CourseName = "数学", Score = 92 }
}
};
context.Students.Add(newStudent);
context.SaveChanges(); // 学生和成绩会一起插入[citation:9]
}
15.4.2 查询数据
csharp
using (var context = new SchoolDbContext())
{
// 1. 获取所有记录
var allStudents = context.Students.ToList();
// 2. 条件查询
var adultStudents = context.Students
.Where(s => s.Age >= 18)
.ToList();
// 3. 获取单个记录
var firstStudent = context.Students.FirstOrDefault();
var studentById = context.Students.Find(1); // 先查缓存,再去数据库
// 4. 排序
var sorted = context.Students
.OrderBy(s => s.Age) // 升序
.ThenBy(s => s.Name) // 然后按姓名排序
.ToList();
var descending = context.Students
.OrderByDescending(s => s.Age) // 降序
.ToList();
// 5. 分页
int pageSize = 10;
int pageNumber = 2;
var page = context.Students
.Skip((pageNumber - 1) * pageSize)
.Take(pageSize)
.ToList();
// 6. 投影(只查需要的列,提高性能)[citation:4]
var namesOnly = context.Students
.Select(s => new { s.Name, s.Age })
.ToList();
// 7. 聚合函数
int count = context.Students.Count();
int maxAge = context.Students.Max(s => s.Age);
int minAge = context.Students.Min(s => s.Age);
double avgAge = context.Students.Average(s => s.Age);
// 8. 判断是否存在
bool exists = context.Students.Any(s => s.Name == "张三");
}
15.4.3 加载关联数据
csharp
using (var context = new SchoolDbContext())
{
// 方式1:预先加载(Eager Loading)—— 推荐
// 一次查询加载学生和对应的成绩
var studentsWithGrades = context.Students
.Include(s => s.Grades) // 加载成绩
.Include(s => s.Courses) // 加载课程
.ToList();
// 嵌套加载:多级关联
var deepLoad = context.Students
.Include(s => s.Grades)
.ThenInclude(g => g.Course) // 成绩下的课程
.ToList();
// 方式2:显式加载(Explicit Loading)
var student = context.Students.FirstOrDefault(s => s.Id == 1);
context.Entry(student).Collection(s => s.Grades).Load();
// 方式3:懒加载(Lazy Loading)—— 需要安装包
// 访问导航属性时会自动查询,不推荐在 Web 应用中使用,会有 N+1 问题
}
15.4.4 更新数据
csharp
using (var context = new SchoolDbContext())
{
// 方式1:查询后修改
var student = context.Students.FirstOrDefault(s => s.Id == 1);
if (student != null)
{
student.Age = 25;
context.SaveChanges();
}
// 方式2:只更新特定字段
var studentToUpdate = new Student { Id = 1, Age = 26 };
context.Students.Attach(studentToUpdate);
context.Entry(studentToUpdate).Property(s => s.Age).IsModified = true;
context.SaveChanges();
// 方式3:批量更新(EF Core 7.0+)
context.Students
.Where(s => s.Age < 18)
.ExecuteUpdate(setters => setters.SetProperty(s => s.Status, "未成年人"));
// 方式4:更新关联实体
var studentWithGrades = context.Students
.Include(s => s.Grades)
.FirstOrDefault(s => s.Id == 1);
if (studentWithGrades != null)
{
var grade = studentWithGrades.Grades.FirstOrDefault(g => g.CourseName == "数学");
if (grade != null)
{
grade.Score = 95;
context.SaveChanges();
}
}
}
15.4.5 删除数据
csharp
using (var context = new SchoolDbContext())
{
// 方式1:查询后删除
var student = context.Students.FirstOrDefault(s => s.Id == 1);
if (student != null)
{
context.Students.Remove(student);
context.SaveChanges();
}
// 方式2:附加后删除(不先查询,提高性能)
var studentToDelete = new Student { Id = 2 };
context.Students.Attach(studentToDelete);
context.Students.Remove(studentToDelete);
context.SaveChanges();
// 方式3:批量删除(EF Core 7.0+)
context.Students
.Where(s => s.Status == "毕业")
.ExecuteDelete();
// 方式4:删除关联数据
var grade = context.Grades.FirstOrDefault(g => g.Id == 10);
if (grade != null)
{
context.Grades.Remove(grade);
context.SaveChanges();
}
}
15.4.6 批量操作
csharp
// EF Core 7.0+ 支持批量更新和删除,直接生成 SQL 执行,不会先查询
using (var context = new SchoolDbContext())
{
// 批量更新
int updated = context.Students
.Where(s => s.Age < 18)
.ExecuteUpdate(setters => setters
.SetProperty(s => s.Status, "未成年人")
.SetProperty(s => s.Age, s => s.Age + 1));
Console.WriteLine($"更新了 {updated} 条记录");
// 批量删除
int deleted = context.Students
.Where(s => s.Status == "已毕业")
.ExecuteDelete();
Console.WriteLine($"删除了 {deleted} 条记录");
}
15.5 性能优化技巧
15.5.1 使用 AsNoTracking 提高只读查询性能
csharp
// 默认情况下,EF Core 会跟踪实体的变化(占用内存和 CPU)
var students = context.Students.ToList(); // 有跟踪
// 对于只读查询,使用 AsNoTracking 禁用跟踪,性能提升 30-50%[citation:4]
var studentsNoTracking = context.Students
.AsNoTracking()
.Where(s => s.Age >= 18)
.ToList();
15.5.2 使用投影减少数据加载
csharp
// ❌ 不好的做法:加载所有列
var students = context.Students.ToList();
// ✅ 好的做法:只加载需要的列
var studentNames = context.Students
.Select(s => new { s.Id, s.Name }) // 只查 ID 和 Name
.ToList();
// 更复杂的投影
var studentSummaries = context.Students
.Select(s => new StudentSummary
{
Name = s.Name,
Age = s.Age,
GradeCount = s.Grades.Count(),
AvgScore = s.Grades.Average(g => g.Score)
})
.ToList();
15.5.3 使用 Include 避免 N+1 查询
csharp
// ❌ 不好的做法:N+1 查询问题
var students = context.Students.ToList();
foreach (var s in students)
{
// 每次循环都查询一次数据库
foreach (var g in s.Grades) // 如果用了懒加载,这里会查数据库
{
Console.WriteLine($"{s.Name}: {g.Score}");
}
}
// ✅ 好的做法:使用 Include 一次加载[citation:4]
var studentsWithGrades = context.Students
.Include(s => s.Grades)
.ToList();
15.5.4 使用 Find 方法利用缓存
csharp
// FirstOrDefault 每次都查数据库 var student1 = context.Students.FirstOrDefault(s => s.Id == 1); var student2 = context.Students.FirstOrDefault(s => s.Id == 1); // 再次查询数据库 // Find 先查本地缓存,没有再查数据库 var student3 = context.Students.Find(1); // 查数据库 var student4 = context.Students.Find(1); // 从缓存返回,不查数据库
15.5.5 性能优化最佳实践总结
| 优化技巧 | 说明 | 适用场景 |
|---|---|---|
AsNoTracking() |
禁用变更跟踪 | 只读查询 |
| 投影(Select) | 只查需要的列 | 不需要全部字段时 |
Include 预加载 |
一次加载关联数据 | 需要关联数据时 |
Find() 优先 |
利用缓存 | 按主键查询 |
| 批量操作 | 一次执行多条 SQL | 大量数据更新/删除 |
| 索引 | 数据库层面优化 | 频繁查询的字段 |
15.6 迁移管理
15.6.1 迁移命令
bash
# 创建迁移(模型修改后执行) dotnet ef migrations add MigrationName # 应用迁移到数据库 dotnet ef database update # 回滚到指定迁移 dotnet ef database update PreviousMigrationName # 删除最后一次迁移 dotnet ef migrations remove # 查看迁移列表 dotnet ef migrations list # 生成 SQL 脚本 dotnet ef migrations script
15.6.2 迁移工作流
text
1. 修改实体类 ↓ 2. dotnet ef migrations add AddNewField ↓ 3. 检查生成的迁移文件 ↓ 4. dotnet ef database update ↓ 5. 数据库结构更新完成
15.6.3 迁移文件示例
csharp
// 迁移文件示例:20240115083000_AddEmailToStudent.cs
public partial class AddEmailToStudent : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
// 升级:添加 Email 列
migrationBuilder.AddColumn<string>(
name: "Email",
table: "Students",
type: "nvarchar(200)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
// 回滚:删除 Email 列
migrationBuilder.DropColumn(
name: "Email",
table: "Students");
}
}
15.7 综合示例:图书管理系统
csharp
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.Linq;
// ========== 实体类 ==========
// 作者实体
[Table("Authors")]
public class Author
{
public int Id { get; set; }
[Required]
[MaxLength(100)]
public string Name { get; set; }
[Column(TypeName = "date")]
public DateTime BirthDate { get; set; }
// 导航属性:一个作者有多本书
public ICollection<Book> Books { get; set; } = new List<Book>();
}
// 图书实体
[Table("Books")]
public class Book
{
public int Id { get; set; }
[Required]
[MaxLength(200)]
public string Title { get; set; }
[Column(TypeName = "decimal(10,2)")]
public decimal Price { get; set; }
public int Pages { get; set; }
public DateTime PublishDate { get; set; }
// 外键
public int AuthorId { get; set; }
// 导航属性:这本书属于哪个作者
[ForeignKey("AuthorId")]
public Author Author { get; set; }
// 多对多:书和借阅者
public ICollection<BorrowRecord> BorrowRecords { get; set; }
}
// 借阅者实体
public class Borrower
{
public int Id { get; set; }
public string Name { get; set; }
public string Phone { get; set; }
public string Email { get; set; }
public ICollection<BorrowRecord> BorrowRecords { get; set; }
}
// 借阅记录(多对多的中间表)
public class BorrowRecord
{
public int Id { get; set; }
public int BookId { get; set; }
public Book Book { get; set; }
public int BorrowerId { get; set; }
public Borrower Borrower { get; set; }
public DateTime BorrowDate { get; set; }
public DateTime? ReturnDate { get; set; }
public bool IsReturned => ReturnDate.HasValue;
}
// ========== DbContext ==========
public class LibraryDbContext : DbContext
{
public DbSet<Author> Authors { get; set; }
public DbSet<Book> Books { get; set; }
public DbSet<Borrower> Borrowers { get; set; }
public DbSet<BorrowRecord> BorrowRecords { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseSqlite("Data Source=library.db");
optionsBuilder.LogTo(Console.WriteLine); // 显示 SQL 日志
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 配置一对多关系
modelBuilder.Entity<Book>()
.HasOne(b => b.Author)
.WithMany(a => a.Books)
.HasForeignKey(b => b.AuthorId)
.OnDelete(DeleteBehavior.Cascade);
// 配置多对多(通过 BorrowRecord)
modelBuilder.Entity<BorrowRecord>()
.HasOne(br => br.Book)
.WithMany(b => b.BorrowRecords)
.HasForeignKey(br => br.BookId);
modelBuilder.Entity<BorrowRecord>()
.HasOne(br => br.Borrower)
.WithMany(b => b.BorrowRecords)
.HasForeignKey(br => br.BorrowerId);
// 设置默认值
modelBuilder.Entity<Book>()
.Property(b => b.PublishDate)
.HasDefaultValueSql("CURRENT_TIMESTAMP");
}
}
// ========== 服务类 ==========
public class LibraryService
{
private readonly LibraryDbContext _context;
public LibraryService(LibraryDbContext context)
{
_context = context;
}
// 添加作者
public Author AddAuthor(string name, DateTime birthDate)
{
var author = new Author
{
Name = name,
BirthDate = birthDate
};
_context.Authors.Add(author);
_context.SaveChanges();
return author;
}
// 添加图书
public Book AddBook(string title, decimal price, int pages, int authorId)
{
var book = new Book
{
Title = title,
Price = price,
Pages = pages,
AuthorId = authorId,
PublishDate = DateTime.Now
};
_context.Books.Add(book);
_context.SaveChanges();
return book;
}
// 借书
public BorrowRecord BorrowBook(int bookId, int borrowerId)
{
var book = _context.Books.Find(bookId);
if (book == null)
throw new Exception("图书不存在");
var borrower = _context.Borrowers.Find(borrowerId);
if (borrower == null)
throw new Exception("借阅者不存在");
var record = new BorrowRecord
{
BookId = bookId,
BorrowerId = borrowerId,
BorrowDate = DateTime.Now
};
_context.BorrowRecords.Add(record);
_context.SaveChanges();
return record;
}
// 还书
public void ReturnBook(int borrowRecordId)
{
var record = _context.BorrowRecords.Find(borrowRecordId);
if (record == null)
throw new Exception("借阅记录不存在");
record.ReturnDate = DateTime.Now;
_context.SaveChanges();
}
// 查询某个作者的所有图书
public List<Book> GetBooksByAuthor(string authorName)
{
return _context.Books
.Include(b => b.Author)
.Where(b => b.Author.Name.Contains(authorName))
.ToList();
}
// 查询某个读者的借阅记录
public List<BorrowRecord> GetBorrowRecordsByBorrower(string borrowerName)
{
return _context.BorrowRecords
.Include(br => br.Book)
.ThenInclude(b => b.Author)
.Include(br => br.Borrower)
.Where(br => br.Borrower.Name.Contains(borrowerName))
.OrderByDescending(br => br.BorrowDate)
.ToList();
}
// 查询未归还的图书
public List<BorrowRecord> GetUnreturnedBooks()
{
return _context.BorrowRecords
.Include(br => br.Book)
.Include(br => br.Borrower)
.Where(br => !br.IsReturned)
.ToList();
}
// 统计每个作者的图书数量
public List<AuthorStats> GetAuthorStatistics()
{
return _context.Authors
.Select(a => new AuthorStats
{
AuthorName = a.Name,
BookCount = a.Books.Count,
AvgPrice = a.Books.Average(b => b.Price),
TotalPages = a.Books.Sum(b => b.Pages)
})
.OrderByDescending(a => a.BookCount)
.ToList();
}
}
public class AuthorStats
{
public string AuthorName { get; set; }
public int BookCount { get; set; }
public decimal AvgPrice { get; set; }
public int TotalPages { get; set; }
}
// ========== 程序入口 ==========
class Program
{
static void Main()
{
using (var context = new LibraryDbContext())
{
// 确保数据库已创建
context.Database.EnsureCreated();
var service = new LibraryService(context);
// 添加作者
var author1 = service.AddAuthor("JK罗琳", new DateTime(1965, 7, 31));
var author2 = service.AddAuthor("刘慈欣", new DateTime(1963, 6, 23));
// 添加图书
service.AddBook("哈利波特与魔法石", 49.8m, 223, author1.Id);
service.AddBook("哈利波特与密室", 59.8m, 251, author1.Id);
service.AddBook("三体", 68.0m, 302, author2.Id);
service.AddBook("三体2:黑暗森林", 78.0m, 470, author2.Id);
// 添加借阅者
var borrower = new Borrower { Name = "张三", Phone = "13800138000", Email = "zhangsan@example.com" };
context.Borrowers.Add(borrower);
context.SaveChanges();
// 借书
var book = context.Books.First(b => b.Title == "三体");
service.BorrowBook(book.Id, borrower.Id);
// 查询
Console.WriteLine("=== 刘慈欣的作品 ===");
var books = service.GetBooksByAuthor("刘慈欣");
foreach (var b in books)
{
Console.WriteLine($"《{b.Title}》 - {b.Price:C} - {b.Pages}页");
}
Console.WriteLine("\n=== 未归还的图书 ===");
var unreturned = service.GetUnreturnedBooks();
foreach (var br in unreturned)
{
Console.WriteLine($"{br.Borrower.Name} 借了《{br.Book.Title}》,借阅日期:{br.BorrowDate:yyyy-MM-dd}");
}
Console.WriteLine("\n=== 作者统计 ===");
var stats = service.GetAuthorStatistics();
foreach (var s in stats)
{
Console.WriteLine($"{s.AuthorName}: {s.BookCount}本书,平均价格{s.AvgPrice:C},总页数{s.TotalPages}");
}
}
}
}
15.8 常见错误与陷阱
错误1:忘记调用 SaveChanges
csharp
// ❌ 错误:添加后没有调用 SaveChanges
context.Students.Add(new Student { Name = "张三" });
// 数据没有保存到数据库!
// ✅ 正确
context.Students.Add(new Student { Name = "张三" });
context.SaveChanges();
错误2:N+1 查询问题
csharp
// ❌ 错误:懒加载导致 N+1 查询
var students = context.Students.ToList();
foreach (var s in students)
{
// 每个学生都会触发一次数据库查询
Console.WriteLine(s.Grades.Count);
}
// ✅ 正确:使用 Include 预先加载
var studentsWithGrades = context.Students.Include(s => s.Grades).ToList();
错误3:在循环中更新数据
csharp
// ❌ 错误:在循环中调用 SaveChanges(多次数据库往返)
foreach (var student in students)
{
student.Age++;
context.SaveChanges(); // 每次都提交
}
// ✅ 正确:循环后统一提交
foreach (var student in students)
{
student.Age++;
}
context.SaveChanges(); // 一次提交
错误4:没有处理并发
csharp
// 并发更新可能导致数据丢失
// 解决方法:使用并发令牌(Concurrency Token)
public class Student
{
public int Id { get; set; }
[ConcurrencyCheck]
public string Name { get; set; }
[Timestamp]
public byte[] RowVersion { get; set; }
}
错误5:查询后修改实体但未保存
csharp
// ❌ 错误:查询后修改但没调用 SaveChanges var student = context.Students.Find(1); student.Name = "新名字"; // 数据没有保存! // ✅ 正确 student.Name = "新名字"; context.SaveChanges();
15.9 本章总结
核心知识点导图
text
EF Core
├── 核心概念
│ ├── ORM:对象关系映射
│ ├── DbContext:数据库会话
│ ├── DbSet<T>:数据表访问
│ └── 实体类:表映射
│
├── 配置方式
│ ├── 数据注解(Data Annotations)
│ └── Fluent API(OnModelCreating)
│
├── 关系映射
│ ├── 一对一(1:1)
│ ├── 一对多(1:N)
│ └── 多对多(N:N)
│
├── CRUD 操作
│ ├── Create:Add, AddRange
│ ├── Read:Find, FirstOrDefault, Where, Include
│ ├── Update:修改属性后 SaveChanges
│ └── Delete:Remove, RemoveRange
│
├── 迁移管理
│ ├── Add-Migration
│ ├── Update-Database
│ └── Remove-Migration
│
└── 性能优化
├── AsNoTracking
├── 投影(Select)
├── 预加载(Include)
└── 批量操作
EF Core 选择指南
| 场景 | 推荐方案 |
|---|---|
| 学习/小型项目 | SQLite + Code First |
| Web 应用 | SQL Server/PostgreSQL + 依赖注入 |
| 高性能只读查询 | AsNoTracking + 投影 |
| 关联数据查询 | Include 预加载 |
| 大批量更新 | ExecuteUpdate (EF Core 7+) |
| 已有数据库 | Database First(反向工程) |
15.10 练习题
基础题
-
创建学生和班级的一对多关系,编写代码添加学生和班级,并查询某个班级的所有学生。
-
实现一个简单的图书管理系统,包含图书的增删改查功能,使用 EF Core + SQLite。
-
使用迁移功能,为实体添加一个新的字段(如学生的 Phone 字段),并更新数据库。
应用题
-
实现一个订单系统:
-
实体:Customer(客户)、Order(订单)、OrderItem(订单项)
-
关系:客户一对多订单,订单一对多订单项
-
实现添加订单、查询客户订单、计算订单总额等功能
-
-
优化一个查询性能问题:
-
原来的查询有 N+1 问题
-
使用 Include 和 AsNoTracking 优化
-
对比优化前后的 SQL 执行次数
-
挑战题
-
实现一个数据访问层基类:
-
泛型 Repository<T>
-
支持 GetById、GetAll、Add、Update、Delete
-
支持条件查询和分页
-
-
实现一个简单的博客系统:
-
支持文章(Post)、分类(Category)、标签(Tag)
-
多对多关系:文章-标签
-
实现文章搜索(标题、内容)
-
实现按分类、按标签筛选
-
更多推荐
所有评论(0)