gorm 的 delete() 默认不级联删除子记录,需依赖数据库外键 on delete cascade 实现事务安全的级联;软删除下级联失效,多对多需手动清理中间表。

事务中直接调用 Delete() 不会级联删除子记录
GORM 的 Delete() 默认只删主表记录,即使你定义了 HasMany 关系,也不会自动查出并删除关联的子记录(比如 Orders 属于 User)。这在事务里尤其危险:主记录被删了,子记录还挂着,外键约束没启用的话,数据就“漏”了。
常见错误现象:db.Transaction(func(tx *gorm.DB) error { tx.Delete(&user); return nil }) 执行完,user 没了,但所有 user.Orders 还在数据库里。
- 这不是 bug,是 GORM 明确的设计——它不模拟数据库行为,只做 ORM 映射
- 预加载
Preload("Orders")也无用:Delete()不读取、不处理已加载的关联切片 - 想靠
BeforeDelete钩子手动删子记录?钩子在事务内执行,但若子表量大或逻辑复杂,容易超时或中断,且无法回滚到钩子内部的失败点
必须靠数据库外键的 ON DELETE CASCADE 保证原子性
唯一能和事务一起安全工作的级联删除,是让数据库引擎自己动手。GORM 只负责建好带级联行为的外键,之后所有 Delete() 调用都会触发数据库原生级联,事务自然包裹整个操作链。
实操建议:
- 在迁移阶段(不是运行时)用
AddForeignKey()添加约束,例如:db.Model(&Order{}).AddForeignKey("user_id", "users(id)", "CASCADE", "NO ACTION") - 确保外键字段名和引用表名完全匹配 GORM 默认约定(如结构体字段
UserID→ 数据库列user_id,目标表名users) - 不要在生产环境用
AutoMigrate()自动加外键——它默认不生成CASCADE,必须显式调用AddForeignKey() - 验证是否生效:连上数据库执行
SHOW CREATE TABLE orders,确认外键定义里有ON DELETE CASCADE
多对多中间表的删除不能依赖外键级联
如果你要删的是多对多关系中的一端(比如删掉一个 Tag,并清除所有 post_tags 中的关联行),外键级联不管用——因为中间表的外键通常指向两个主表,ON DELETE CASCADE 只能配一个方向。
正确做法是在事务里显式清理中间表:
- 先查出所有待解绑的关联 ID:
tx.Table("post_tags").Where("tag_id = ?", tag.ID).Select("post_id").Rows() - 再批量删除中间记录:
tx.Where("tag_id = ?", tag.ID).Delete(&PostTag{}) - 或者更稳妥:用
tx.Exec("DELETE FROM post_tags WHERE tag_id = ?", tag.ID),绕过 GORM 关联逻辑,避免预加载干扰 - 注意别漏掉事务上下文:所有操作都得用
tx,不能混用db
软删除场景下级联逻辑完全失效
一旦主模型启用了 gorm.DeletedAt(即软删除),ON DELETE CASCADE 就不会触发——因为数据库实际执行的是 UPDATE,不是 DELETE。此时子表记录不会被标记为软删除,也不会被物理删除。
应对方式只有两个:
- 放弃软删除,改用硬删除 + 归档表,让外键级联真正起作用
- 手动补全逻辑:在事务中,先
UpdateColumn("deleted_at", now)主记录,再对每个子模型执行相同操作(需确保子模型也定义了DeletedAt) - 千万别用
Delete()带Unscoped()去“强行硬删”——这会跳出事务保护,且破坏软删除语义
最常被忽略的点:外键级联只响应 DELETE 语句,而 GORM 的软删除本质是 UPDATE;只要用了软删除,所有级联行为就得重写成应用层逻辑,没有捷径。











