Skip to content

级联删除行为 ​

概述 ​

级联删除(Cascade Delete)是数据库关系中的重要概念,指当删除主表记录时,自动删除或处理相关从表记录的行为。EF Core 提供了多种级联删除策略,合理配置可以避免孤儿记录和外键约束错误。


级联删除类型 ​

1. Cascade (级联删除) ⭐⭐⭐⭐⭐ ​

行为: 删除主实体时,自动删除所有相关从实体

csharp
modelBuilder.Entity<Order>(entity =>
{
    entity.HasOne(o => o.Customer)
          .WithMany(c => c.Orders)
          .HasForeignKey(o => o.CustomerId)
          .OnDelete(DeleteBehavior.Cascade); // 默认行为
});

数据库效果:

sql
-- 生成的 SQL
ALTER TABLE Orders 
ADD CONSTRAINT FK_Orders_Customers 
FOREIGN KEY (CustomerId) REFERENCES Customers(Id) 
ON DELETE CASCADE;

示例:

csharp
var customer = await context.Customers
    .Include(c => c.Orders)
    .FirstOrDefaultAsync(c => c.Id == 1);

context.Customers.Remove(customer);
await context.SaveChangesAsync();

// 结果: Customer 和所有相关 Orders 都被删除

适用场景:

  • ✅ 强依赖关系(订单明细依赖于订单)
  • ✅ 生命周期一致的实体
  • ✅ 不需要保留历史数据

2. Restrict / NoAction (限制删除) ​

行为: 如果存在相关从实体,则阻止删除主实体

csharp
modelBuilder.Entity<Customer>(entity =>
{
    entity.HasMany(c => c.Orders)
          .WithOne(o => o.Customer)
          .HasForeignKey(o => o.CustomerId)
          .OnDelete(DeleteBehavior.Restrict);
});

示例:

csharp
var customer = await context.Customers.FindAsync(1);
context.Customers.Remove(customer);

// ❌ 抛出异常: 存在相关订单
await context.SaveChangesAsync();
// DbUpdateException: The DELETE statement conflicted with the REFERENCE constraint

正确处理:

csharp
var customer = await context.Customers
    .Include(c => c.Orders)
    .FirstOrDefaultAsync(c => c.Id == 1);

// 先删除或重新分配订单
context.Orders.RemoveRange(customer.Orders);
// 或者: customer.Orders.ToList().ForEach(o => o.CustomerId = null);

// 再删除客户
context.Customers.Remove(customer);
await context.SaveChangesAsync(); // ✅ 成功

适用场景:

  • ✅ 重要数据保护(防止误删)
  • ✅ 需要手动控制删除逻辑
  • ✅ 有业务规则约束

3. SetNull (设置为空) ​

行为: 删除主实体时,将从实体的外键设置为 NULL

csharp
modelBuilder.Entity<Employee>(entity =>
{
    entity.HasOne(e => e.Department)
          .WithMany(d => d.Employees)
          .HasForeignKey(e => e.DepartmentId)
          .OnDelete(DeleteBehavior.SetNull);
});

前提条件: 外键必须可为空

csharp
public class Employee
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // ✅ 可空外键
    public int? DepartmentId { get; set; }
    public Department? Department { get; set; }
}

示例:

csharp
var dept = await context.Departments.FindAsync(1);
context.Departments.Remove(dept);
await context.SaveChangesAsync();

// 结果: Department 被删除,相关 Employee 的 DepartmentId 变为 NULL

适用场景:

  • ✅ 可选关系(员工可以没有部门)
  • ✅ 从实体可以独立存在
  • ❌ 不适用于必需关系

4. ClientSetNull (客户端设置为空) ​

行为: EF Core 在内存中将从实体的外键设置为 NULL,但不更新数据库

csharp
modelBuilder.Entity<Post>(entity =>
{
    entity.HasOne(p => p.Author)
          .WithMany(a => a.Posts)
          .HasForeignKey(p => p.AuthorId)
          .OnDelete(DeleteBehavior.ClientSetNull);
});

示例:

csharp
var author = await context.Authors
    .Include(a => a.Posts)
    .FirstOrDefaultAsync(a => a.Id == 1);

context.Authors.Remove(author);
await context.SaveChangesAsync();

// 结果:
// - Author 被删除
// - Posts 的外键在内存中被设为 NULL
// - 但数据库中的外键仍为原值(可能导致不一致!)

注意: ⚠️ 通常不推荐使用,容易导致数据不一致


5. ClientCascade (客户端级联) ​

行为: EF Core 在内存中级联删除,但不配置数据库级联

csharp
.OnDelete(DeleteBehavior.ClientCascade);

适用场景:

  • 数据库不支持级联删除
  • 需要在保存前执行自定义逻辑

删除行为对比表 ​

行为数据库配置内存行为外键要求风险
CascadeON DELETE CASCADE标记从实体删除任意可能意外删除大量数据
RestrictNO ACTION抛出异常任意需手动处理
SetNullON DELETE SET NULL外键设为NULL可空可能产生孤儿记录
ClientSetNull无外键设为NULL可空数据不一致风险
ClientCascade无标记从实体删除任意需手动保存

实际应用场景 ​

场景 1: 博客系统(级联删除) ​

csharp
public class Blog
{
    public int Id { get; set; }
    public string Title { get; set; }
    
    public ICollection<Post> Posts { get; set; } = new List<Post>();
}

public class Post
{
    public int Id { get; set; }
    public string Content { get; set; }
    public int BlogId { get; set; }
    
    public Blog? Blog { get; set; }
    public ICollection<Comment> Comments { get; set; } = new List<Comment>();
}

public class Comment
{
    public int Id { get; set; }
    public string Text { get; set; }
    public int PostId { get; set; }
    
    public Post? Post { get; set; }
}

// 配置
modelBuilder.Entity<Blog>(entity =>
{
    entity.HasMany(b => b.Posts)
          .WithOne(p => p.Blog)
          .HasForeignKey(p => p.BlogId)
          .OnDelete(DeleteBehavior.Cascade); // 删除博客时删除所有文章
});

modelBuilder.Entity<Post>(entity =>
{
    entity.HasMany(p => p.Comments)
          .WithOne(c => c.Post)
          .HasForeignKey(c => c.PostId)
          .OnDelete(DeleteBehavior.Cascade); // 删除文章时删除所有评论
});

// 使用
var blog = await context.Blogs
    .Include(b => b.Posts)
        .ThenInclude(p => p.Comments)
    .FirstOrDefaultAsync(b => b.Id == 1);

context.Blogs.Remove(blog);
await context.SaveChangesAsync();

// 结果: Blog → Posts → Comments 全部删除

场景 2: 订单系统(限制删除) ​

csharp
public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public ICollection<Order> Orders { get; set; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public DateTime OrderDate { get; set; }
    public int CustomerId { get; set; }
    
    public Customer? Customer { get; set; }
}

// 配置: 有订单的客户不能删除
modelBuilder.Entity<Customer>(entity =>
{
    entity.HasMany(c => c.Orders)
          .WithOne(o => o.Customer)
          .HasForeignKey(o => o.CustomerId)
          .OnDelete(DeleteBehavior.Restrict);
});

// 安全删除服务
public class CustomerService
{
    public async Task<bool> SafeDeleteCustomerAsync(int customerId)
    {
        var customer = await context.Customers
            .Include(c => c.Orders)
            .FirstOrDefaultAsync(c => c.Id == customerId);
        
        if (customer == null)
            return false;
        
        // 检查是否有订单
        if (customer.Orders.Any())
        {
            // 选项 1: 拒绝删除
            throw new InvalidOperationException("Cannot delete customer with orders");
            
            // 选项 2: 归档订单
            // foreach (var order in customer.Orders)
            // {
            //     order.CustomerId = null; // 设为匿名订单
            // }
        }
        
        context.Customers.Remove(customer);
        await context.SaveChangesAsync();
        return true;
    }
}

场景 3: 分类系统(设置为空) ​

csharp
public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    public ICollection<Product> Products { get; set; } = new List<Product>();
}

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    
    // 可空外键: 产品可以没有分类
    public int? CategoryId { get; set; }
    public Category? Category { get; set; }
}

// 配置: 删除分类时,产品变为未分类
modelBuilder.Entity<Category>(entity =>
{
    entity.HasMany(c => c.Products)
          .WithOne(p => p.Category)
          .HasForeignKey(p => p.CategoryId)
          .OnDelete(DeleteBehavior.SetNull);
});

// 使用
var category = await context.Categories.FindAsync(1);
context.Categories.Remove(category);
await context.SaveChangesAsync();

// 结果: Category 被删除,相关 Product 的 CategoryId 变为 NULL

循环级联问题 ​

问题描述 ​

SQL Server 不允许循环级联路径:

A → B (Cascade)
B → C (Cascade)
A → C (Cascade)  ← 报错!

解决方案 ​

方案 1: 打破循环 ​

csharp
// ❌ 会导致循环级联
modelBuilder.Entity<A>().HasMany(a => a.Bs).WithOne(b => b.A).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<B>().HasMany(b => b.Cs).WithOne(c => c.B).OnDelete(DeleteBehavior.Cascade);
modelBuilder.Entity<A>().HasMany(a => a.Cs).WithOne(c => c.A).OnDelete(DeleteBehavior.Cascade);

// ✅ 修改其中一个为 Restrict
modelBuilder.Entity<A>().HasMany(a => a.Cs).WithOne(c => c.A).OnDelete(DeleteBehavior.Restrict);

方案 2: 手动处理删除 ​

csharp
public async Task DeleteCategoryAsync(int categoryId)
{
    var category = await context.Categories
        .Include(c => c.Products)
            .ThenInclude(p => p.Reviews)
        .FirstOrDefaultAsync(c => c.Id == categoryId);
    
    if (category == null) return;
    
    // 手动删除层级关系
    foreach (var product in category.Products)
    {
        context.Reviews.RemoveRange(product.Reviews);
    }
    context.Products.RemoveRange(category.Products);
    context.Categories.Remove(category);
    
    await context.SaveChangesAsync();
}

最佳实践 ​

✅ 推荐做法 ​

1. 显式配置删除行为 ​

csharp
// ❌ 依赖默认行为
entity.HasOne(o => o.Customer).WithMany(c => c.Orders);

// ✅ 明确指定
entity.HasOne(o => o.Customer)
      .WithMany(c => c.Orders)
      .OnDelete(DeleteBehavior.Cascade);

2. 根据业务需求选择策略 ​

csharp
// 强依赖: Cascade
.HasOne(detail => detail.Order)
    .WithMany(order => order.Details)
    .OnDelete(DeleteBehavior.Cascade);

// 弱依赖: SetNull
.HasOne(emp => emp.Department)
    .WithMany(dept => dept.Employees)
    .OnDelete(DeleteBehavior.SetNull);

// 重要数据: Restrict
.HasOne(customer => customer.Orders)
    .WithMany(order => order.Customer)
    .OnDelete(DeleteBehavior.Restrict);

3. 实现软删除替代硬删除 ​

csharp
public interface ISoftDelete
{
    bool IsDeleted { get; set; }
    DateTime? DeletedAt { get; set; }
}

public class Order : ISoftDelete
{
    public int Id { get; set; }
    public bool IsDeleted { get; set; }
    public DateTime? DeletedAt { get; set; }
}

// 全局查询过滤器
modelBuilder.Entity<Order>()
    .HasQueryFilter(o => !o.IsDeleted);

// 软删除
public async Task SoftDeleteOrderAsync(int orderId)
{
    var order = await context.Orders.FindAsync(orderId);
    order.IsDeleted = true;
    order.DeletedAt = DateTime.UtcNow;
    await context.SaveChangesAsync();
}

优势:

  • ✅ 保留历史数据
  • ✅ 可恢复
  • ✅ 避免级联删除风险

❌ 避免的陷阱 ​

1. 不要在不理解的情况下使用 Cascade ​

csharp
// ❌ 危险: 可能删除大量数据
.OnDelete(DeleteBehavior.Cascade);

// ✅ 评估影响后再决定
if (entity.Orders.Count > 1000)
{
    // 考虑 Restrict + 手动处理
}

2. 不要忘记 Include 相关实体 ​

csharp
// ❌ 可能导致意外行为
var customer = await context.Customers.FindAsync(1);
context.Customers.Remove(customer);

// ✅ 明确加载并检查
var customer = await context.Customers
    .Include(c => c.Orders)
    .FirstOrDefaultAsync(c => c.Id == 1);

if (customer.Orders.Any())
{
    // 处理订单
}

3. 注意性能影响 ​

csharp
// ❌ 级联删除大量数据可能很慢
var blog = await context.Blogs
    .Include(b => b.Posts)
        .ThenInclude(p => p.Comments)
    .FirstOrDefaultAsync(b => b.Id == 1);
// 假设有 10000 篇文章,每篇 100 条评论 = 100 万条记录!

// ✅ 使用批量删除
await context.Database.ExecuteSqlRawAsync(
    "DELETE FROM Comments WHERE PostId IN (SELECT Id FROM Posts WHERE BlogId = {0})", 
    blogId);

await context.Database.ExecuteSqlRawAsync(
    "DELETE FROM Posts WHERE BlogId = {0}", 
    blogId);

await context.Blogs.Where(b => b.Id == blogId).ExecuteDeleteAsync();

总结 ​

选择指南 ​

关系类型推荐行为示例
强依赖Cascade订单→订单明细
可选依赖SetNull产品→分类
重要数据Restrict客户→订单
历史数据软删除所有实体

核心原则 ​

  1. 明确意图: 显式配置删除行为
  2. 业务优先: 根据业务规则选择策略
  3. 数据安全: 重要数据使用软删除
  4. 性能考虑: 大批量删除使用批量操作
  5. 测试验证: 测试各种删除场景

合理配置级联删除,既能保证数据完整性,又能提升开发效率! 🚀

基于 MIT 许可发布