Appearance
级联删除行为
概述
级联删除(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);适用场景:
- 数据库不支持级联删除
- 需要在保存前执行自定义逻辑
删除行为对比表
| 行为 | 数据库配置 | 内存行为 | 外键要求 | 风险 |
|---|---|---|---|---|
| Cascade | ON DELETE CASCADE | 标记从实体删除 | 任意 | 可能意外删除大量数据 |
| Restrict | NO ACTION | 抛出异常 | 任意 | 需手动处理 |
| SetNull | ON 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 | 客户→订单 |
| 历史数据 | 软删除 | 所有实体 |
核心原则
- 明确意图: 显式配置删除行为
- 业务优先: 根据业务规则选择策略
- 数据安全: 重要数据使用软删除
- 性能考虑: 大批量删除使用批量操作
- 测试验证: 测试各种删除场景
合理配置级联删除,既能保证数据完整性,又能提升开发效率! 🚀