物理删除必须显式调用Unscoped(),否则Delete()仅软删;Unscoped()会全局跳过软删除过滤,影响Preload等后续操作,批量删除前应先Count校验,恢复软删记录后需重新查询或条件锁定再物理删除。

物理删除必须显式调用 Unscoped()
GORM 的 Delete() 默认只更新 DeletedAt 字段,不会真正从数据库删掉行。要执行物理删除(即发出 DELETE FROM 语句),必须在操作链最前端加 Unscoped()。它不是可选修饰,而是绕过软删除拦截层的强制开关。
-
db.Unscoped().Delete(&user):单条物理删除,依赖user.ID有效 -
db.Unscoped().Where("status = ?", "draft").Delete(&Post{}):按条件批量物理删除,不依赖实例主键,更安全 - 错误写法:
db.Delete(&user)或db.Where(...).Delete(&user)—— 这俩都只会软删,哪怕你心里想的是“彻底清掉”
Unscoped() 的影响范围容易被低估
Unscoped() 是链式方法,一旦调用,后续所有操作(包括 Preload 关联查询)都会跳过软删除过滤。比如:
db.Unscoped().Preload("Orders").First(&user, 123) 会把用户和他所有订单(含已软删的订单)一起查出来——这在审计或回收站场景合理,但在用户中心接口里就是严重数据泄露。
- 别在通用 DAO 层无差别加
Unscoped(),它不该是默认行为 - 批量物理删除前,建议先用
Unscoped().Where(...).Count(&n)确认数量,避免误删 - 事务中做物理删除时,注意其他协程可能正通过普通查询读取该记录,存在短暂不一致窗口
恢复软删记录后立刻物理删除?别跳步
有时业务需要“先恢复再删”,比如误删后审核确认再清空。但不能直接 db.Unscoped().Model(&u).Update("deleted_at", nil) 后紧接 db.Delete(&u) —— 中间没刷新状态,第二次 Delete() 仍会走软删逻辑。
- 正确做法:恢复后重新查一次(带
Unscoped()),确保DeletedAt已为nil,再执行Unscoped().Delete() - 或者一步到位:
db.Unscoped().Where("id = ? AND deleted_at IS NOT NULL", u.ID).Delete(&User{}),用条件锁定“仅对已软删的记录物理清除” - 别用
Save()手动赋值DeletedAt = time.Time{},零值时间仍被 GORM 视为“已删”
索引缺失会让物理清除变慢,但不影响功能
DeletedAt 字段加 gorm:"index" 主要优化的是常规查询(WHERE deleted_at IS NULL)的性能。物理删除本身走的是主键或 WHERE 条件,不直接受该索引影响。
- 但如果你常跑
Unscoped().Where("deleted_at IS NOT NULL").Delete(&User{})这类“清空回收站”操作,没索引会导致全表扫描 - 建索引命令示例:
CREATE INDEX idx_users_deleted_at ON users(deleted_at); - 嵌入
gorm.Model会自动加索引;自定义字段务必显式写gorm:"index",漏掉不会报错,只是后期扛不住量
Unscoped() 的透传性会让 Preload 和 Joins 也失效,这点在复杂关联模型里几乎必然踩坑。


















