物理删除必须用Unscoped().Delete()绕过软删除机制,否则Delete()默认执行软删除;批量物理删除需Unscoped().Where().Delete(),且应手动处理关联数据并慎用Unscoped()避免污染作用域。

物理删除必须显式禁用软删除机制
GORM 默认开启软删除(即给记录加 deleted_at 时间戳而非真正移除),直接调用 Delete() 会触发软删除,不是物理删除。要物理删除,必须绕过 GORM 的软删除钩子和 WHERE 条件自动注入。
- 对单条记录:用
Unscoped().Delete()—— 它会跳过WHERE deleted_at IS NULL过滤,但注意仍会执行模型定义的BeforeDelete钩子 - 对批量记录:必须用
Unscoped().Where(...).Delete(&Model{}),且Where条件里不能含deleted_at相关判断,否则可能被二次过滤 - 更彻底的方式是跳过 GORM ORM 层,改用
Exec()执行原生 SQL:db.Exec("DELETE FROM users WHERE id = ?", 123),完全规避钩子和作用域逻辑
慎用 Unscoped():它不只影响删除,还影响查询和更新
Unscoped() 是全局作用域开关,一旦启用,后续所有操作(包括 Find()、Update())都会无视软删除标记,容易意外读到/改到已“删除”的数据。
- 不要在事务外长期持有
Unscoped()实例,比如db.Unscoped()赋值给全局变量 - 推荐写法是链式调用:
db.Unscoped().Where("id = ?", id).Delete(&User{}),作用域仅限当前语句 - 若需多次物理操作,建议封装为独立函数,并在函数内新建
Unscoped()实例,避免污染外部 db 句柄
物理删除前务必手动处理关联数据
GORM 的 CASCADE 删除只对软删除生效(通过外键 ON DELETE SET NULL 或触发器配合),物理删除时不会自动清理关联表——这会导致外键约束失败或数据不一致。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 检查模型是否定义了
foreignKey和constraint标签,例如UserID uint `gorm:"foreignKey:UserID;constraint:OnDelete:CASCADE;"`,但这依赖数据库层面的外键,不是 GORM 行为 - 稳妥做法是显式删除依赖项:
db.Where("user_id = ?", userID).Delete(&Order{}),再删主表 - 若用原生 SQL 物理删除,必须自己写多语句或使用数据库事务包裹,否则可能留下孤儿记录
注意 MySQL 与 PostgreSQL 对 DELETE 返回值的差异
GORM Delete() 方法返回的 RowsAffected 在不同驱动下行为不一致:MySQL 返回实际删除行数,PostgreSQL 在某些版本中可能返回 -1(尤其带子查询或 CTE 时)。
立即学习“go语言免费学习笔记(深入)”;
- 不要依赖
result.RowsAffected == 0判断“记录不存在”,应先First()查是否存在,或用Where(...).Select("id").Find(&ids)预检 - 物理删除后若需确认结果,优先查主键是否存在,而不是信
RowsAffected - 在测试中跨数据库验证物理删除逻辑,特别是涉及
RETURNING(PostgreSQL)或LAST_INSERT_ID()(MySQL)的场景
物理删除没有后悔药,GORM 的抽象层反而容易让人忽略底层 SQL 真实行为。最安全的做法往往是:明确意图 → 关闭作用域 → 手动清理关联 → 用原生 SQL 或最小化 ORM 调用 → 数据库层加外键约束兜底。

















