跳到主要内容

级联操作默认值

概述

本文档描述关系级联操作的默认行为。当用户未显式指定 onDeleteonUpdate 时,系统会根据关系类型和是否可为空(nullable)自动应用合理的默认值。

默认规则

1. 一对一关系(ONE_TO_ONE)

{
onDelete: OnDeleteAction.CASCADE, // 删除父实体时自动删除子实体
onUpdate: OnUpdateAction.RESTRICT // 禁止更新主键
}

设计理由:一对一关系中子实体完全依赖父实体,父实体删除时子实体失去存在意义。

2. 一对多关系(ONE_TO_MANY)

{
onDelete: OnDeleteAction.CASCADE, // 删除父实体时自动删除所有子实体
onUpdate: OnUpdateAction.RESTRICT // 禁止更新主键
}

设计理由:一对多关系中多个子实体依赖同一个父实体,父实体删除时子实体失去存在意义。

示例:订单与订单项,删除订单时应该删除所有订单项。

3. 多对一关系(MANY_TO_ONE)

3.1 不可为空(nullable=false)

{
onDelete: OnDeleteAction.RESTRICT, // 禁止删除被引用的父实体
onUpdate: OnUpdateAction.RESTRICT // 禁止更新主键
}

设计理由:如果关系不可为空,说明子实体必须有父实体。禁止删除父实体可以保证数据完整性。

示例:订单项必须属于某个订单,如果订单有订单项则不允许删除订单。

3.2 可为空(nullable=true)

{
onDelete: OnDeleteAction.SET_NULL, // 删除父实体时将子实体的外键设为 NULL
onUpdate: OnUpdateAction.RESTRICT // 禁止更新主键
}

设计理由:如果关系可为空,说明子实体可以独立存在。删除父实体时将外键设为 NULL 而不是删除子实体。

示例:产品可以有分类也可以没有分类,删除分类时产品的分类ID设为NULL。

4. 多对多关系(MANY_TO_MANY)

{
onDelete: OnDeleteAction.RESTRICT, // 禁止删除有关联的实体
onUpdate: OnUpdateAction.RESTRICT // 禁止更新主键
}

设计理由:多对多关系通过中间表实现,删除任一方都不应该自动影响另一方。应该先显式解除关联,再删除实体。

示例:学生和课程的多对多关系,删除学生不应该删除课程,反之亦然。应该先从选课表中删除记录。

onUpdate 为什么都是 RESTRICT?

在关系型数据库的最佳实践中,主键不应该被更新

  1. 主键的职责:主键用于唯一标识一行数据,应该是不可变的
  2. 性能考虑:更新主键需要同步更新所有外键引用,开销大
  3. 数据完整性:更新主键容易导致引用关系混乱
  4. 业务语义:如果需要"修改"实体,通常应该是创建新实体并归档旧实体,而不是更新主键

因此,所有关系类型的 onUpdate 默认都是 RESTRICT

使用示例

自动应用默认值

@Entity()
class Order {
@OneToMany(() => OrderItem, 'order')
items!: OrderItem[];
// 自动应用: onDelete=CASCADE, onUpdate=RESTRICT
}

覆盖默认值

@Entity()
class Order {
@OneToMany(() => OrderItem, 'order', {
onDelete: OnDeleteAction.SET_NULL, // 覆盖默认的 CASCADE
onUpdate: OnUpdateAction.RESTRICT // 保持默认值
})
items!: OrderItem[];
}

典型场景

场景1:订单与订单项(使用默认值)

@Entity()
class Order {
@PrimaryGeneratedColumn()
id!: number;

@OneToMany(() => OrderItem, 'order')
items!: OrderItem[];
// 默认: CASCADE delete,删除订单时自动删除所有订单项
}

@Entity()
class OrderItem {
@PrimaryGeneratedColumn()
id!: number;

@ManyToOne(() => Order, 'items', { nullable: false })
order!: Order;
// 默认: RESTRICT delete,如果订单有订单项则不允许删除订单
// 注意:这里有冲突!需要用户显式指定行为
}

冲突解决:订单项的 ManyToOne 应该允许级联删除:

@ManyToOne(() => Order, 'items', {
nullable: false,
onDelete: OnDeleteAction.CASCADE // 订单删除时,订单项也删除
})
order!: Order;

场景2:产品与分类(可选关系)

@Entity()
class Category {
@PrimaryGeneratedColumn()
id!: number;

@OneToMany(() => Product, 'category')
products!: Product[];
// 默认: CASCADE delete,但通常不希望删除分类时删除产品
// 应该覆盖为 SET_NULL
}

@Entity()
class Product {
@PrimaryGeneratedColumn()
id!: number;

@ManyToOne(() => Category, 'products', { nullable: true })
category?: Category;
// 默认: SET_NULL delete,删除分类时产品的分类ID设为NULL ✅
}

场景3:学生与课程(多对多)

@Entity()
class Student {
@PrimaryGeneratedColumn()
id!: number;

@ManyToMany(() => Course, 'students', {
junctionEntityType: () => Enrollment
})
courses!: Course[];
// 默认: RESTRICT delete,如果学生有选课则不允许删除学生
// 需要先从 Enrollment 表中删除选课记录
}

注意事项

  1. 默认值不一定适合所有场景:请根据实际业务需求评估是否需要覆盖默认值

  2. 一对多和多对一的级联冲突

    • 一对多默认 CASCADE delete
    • 多对一(不可为空)默认 RESTRICT delete
    • 这两个设置会产生冲突,需要至少一方显式指定行为
  3. 多对多关系的删除

    • 默认 RESTRICT 意味着需要先清理中间表
    • 可以考虑在中间表实体上设置 CASCADE 来自动清理
  4. 性能考虑

    • CASCADE 会触发连锁删除,影响性能
    • 对于大量数据的删除操作,建议使用批量删除而非级联删除

参考资料