第十四章我们学习了异步编程,知道了如何让程序在执行耗时操作时保持响应。但我们的数据仍然存储在内存中,程序一关闭就丢了。上一章我们学习了文件 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 练习题

基础题

  1. 创建学生和班级的一对多关系,编写代码添加学生和班级,并查询某个班级的所有学生。

  2. 实现一个简单的图书管理系统,包含图书的增删改查功能,使用 EF Core + SQLite。

  3. 使用迁移功能,为实体添加一个新的字段(如学生的 Phone 字段),并更新数据库。

应用题

  1. 实现一个订单系统:

    • 实体:Customer(客户)、Order(订单)、OrderItem(订单项)

    • 关系:客户一对多订单,订单一对多订单项

    • 实现添加订单、查询客户订单、计算订单总额等功能

  2. 优化一个查询性能问题:

    • 原来的查询有 N+1 问题

    • 使用 Include 和 AsNoTracking 优化

    • 对比优化前后的 SQL 执行次数

挑战题

  1. 实现一个数据访问层基类:

    • 泛型 Repository<T>

    • 支持 GetById、GetAll、Add、Update、Delete

    • 支持条件查询和分页

  2. 实现一个简单的博客系统:

    • 支持文章(Post)、分类(Category)、标签(Tag)

    • 多对多关系:文章-标签

    • 实现文章搜索(标题、内容)

    • 实现按分类、按标签筛选

更多推荐