数据库外键级联(on delete cascade)是唯一可靠方案,gorm应用层钩子无法保障原子性,必须由数据库引擎执行;多对多中间表需用association().delete()显式清理;软删除下外键级联失效,须手动处理。

数据库外键级联(ON DELETE CASCADE)是唯一可靠方案
GORM 本身不提供应用层级联删除的原子保障,BeforeDelete 或 AfterDelete 钩子无法解决并发下关联记录被其他事务修改、或未预加载导致漏删的问题。真正安全的做法是把级联逻辑交给数据库引擎——通过外键约束声明 CASCADE,由 MySQL/PostgreSQL 等在执行 DELETE FROM companies WHERE id = ? 时自动清理所有 employees 和 roles 记录。
必须在迁移阶段显式添加约束,GORM 的 AutoMigrate() 默认不会创建带 ON DELETE CASCADE 的外键:
db.Model(&Employee{}).AddForeignKey(
"company_id",
"companies(id)",
"CASCADE", // ← 关键:触发子记录自动删除
"NO ACTION",
).Error
- 表名必须小写复数(如
companies),与 GORM 默认约定一致;列名用下划线(如company_id),而非结构体字段名CompanyID - 若已存在表,需先
DROP FOREIGN KEY再重建,否则AddForeignKey会报错 - PostgreSQL 用户注意:GORM v1.25+ 支持
OnUpdate/OnDeletetag,但旧版仍需手动建约束
多对多中间表的关联删除不能靠外键级联
像 user_roles 这类中间表,外键通常指向两个主表(user_id → users.id, role_id → roles.id),但你删除一个 User 时,并不想连带删掉所有 Role 实体——所以不能对 role_id 字段设 CASCADE,否则会误删角色数据。
此时必须用 GORM 的 Association().Delete() 显式清理中间表行:
db.Model(&user).Association("Roles").Delete(&role)
- 该操作只发一条
DELETE FROM user_roles WHERE user_id = ? AND role_id = ?,不查也不改roles表 - 传参可以是
&Role{ID: 5}(无需预加载),也可以是[]uint{1, 3, 7}批量删 - 别用
Save()修改user.Roles切片再保存——GORM 不会 diff 差异,也不会删中间表
软删除(Soft Delete)下级联行为完全失效
如果你启用了 GORM 的软删除(即模型含 DeletedAt gorm.DeletedAt),那么即使数据库外键设了 CASCADE,它也只对物理删除生效。而 db.Delete(&company) 实际执行的是 UPDATE companies SET deleted_at = NOW() WHERE id = ?,数据库根本不会触发外键级联。
- 软删除场景下,必须手动处理关联:先查出所有待“软删”的
Employee,再批量Update("deleted_at", time.Now()) - 或者放弃软删除,改用状态字段(如
status string)+ 普通外键级联,更符合关系一致性 -
Unscoped().Delete()能物理删主表,但依然不会自动级联删子表——因为外键级联只响应 DML 的DELETE,不响应 GORM 的逻辑封装
关联字段 tag 中的 cascade 参数是误导性文档
部分旧版文档提到 gorm:"foreignKey:CompanyID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE",但这仅影响 GORM 自动生成外键语句时的 DDL 输出,**不改变运行时行为**,且在 GORM v2 中已被标记为 deprecated。
- 这个 tag 不会让
Delete()调用自动触发子记录删除;它只是告诉 GORM “建表时请带上 CASCADE” - 如果数据库本身不支持或未启用外键(如 SQLite 默认关,MySQL MyISAM 引擎),该 tag 完全无效
- 生产环境务必验证:连接数据库后执行
SHOW CREATE TABLE employees;,确认外键定义里真有ON DELETE CASCADE
DELETE 语句内。如果你用 GORM 分多次调用 Delete()(比如先删 User,再删 Order),中间任何一步失败都会导致数据不一致——所以必须依赖数据库外键,而不是靠应用层拼 SQL 或事务包裹多个 Delete。











