gorm软删除需deletedat为gorm.deletedat类型(即time.time)并嵌入gorm.model或加gorm:"index"标签,否则delete()执行物理删除;默认查询自动过滤软删记录,查全部须unscoped()前置。

软删除不是加个字段就自动生效,GORM 只认 gorm.DeletedAt 类型;Delete() 默认走 UPDATE 而非 DELETE 语句,但前提是模型结构正确、主键有效,否则可能误删全表或查不到新记录。
为什么调用 Delete() 后查不到记录?
这不是 bug,是软删除机制的默认行为:所有常规查询(First()、Find()、Where().Find())都会自动追加 WHERE deleted_at IS NULL 条件。
- 新记录查不到?大概率是
DeletedAt字段类型写成了time.Time(值类型),零值0001-01-01 00:00:00 +0000 UTC被 GORM 判定为“已删除” - 正确写法只有两种:
gorm.Model(推荐,自带DeletedAt *time.Time) 或显式声明DeletedAt gorm.DeletedAt `gorm:"index"` - 漏掉
gorm:"index"不影响功能,但高并发时WHERE deleted_at IS NULL查询可能变慢
如何安全执行软删除与恢复?
软删除本质是更新 DeletedAt 字段,恢复就是把它设回 nil——这两步都必须绕过默认过滤,否则无效。
- 软删除单条:
db.Delete(&user)→ 生成UPDATE ... SET deleted_at = NOW() - 恢复单条:
db.Unscoped().Model(&user).Update("deleted_at", nil)(注意是nil,不是time.Time{}) - 物理删除单条:
db.Unscoped().Delete(&user)→ 真正执行DELETE FROM,且不触发BeforeDelete钩子 - 批量软删除:
db.Where("status = ?", "draft").Delete(&Post{}),安全,不依赖实例主键
Unscoped() 的透传性极易引发关联查询错误
Unscoped() 是链式方法,一旦前置,它会影响整个调用链,包括后续的 Preload() 关联加载——这点在带外键关系的接口里最常踩坑。
- 例如:
db.Unscoped().Preload("Orders").First(&user, 123),不仅user本身会查到已软删记录,连关联的Orders表也会跳过软删除过滤 - 业务主流程(如用户列表页)不该用
Unscoped();它只适合回收站、审计日志、后台管理等明确需要“绕过过滤”的场景 - 事务中做恢复+更新操作,必须放在同一事务内,否则其他协程可能读到中间状态(比如
deleted_at已清空但其他字段未更新完)
为什么删完发现整张表数据没了?
根本原因是 Delete() 依赖主键定位记录,若传入 struct 实例的主键是零值(如 ID = 0 或空字符串),GORM 会误判为“无条件删除”,某些数据库配置下可能触发全表删除。
- 绝对避免:
var u User; db.Delete(&u)—— 此时u.ID是 0,危险 - 安全做法:先
First()查出再删,或改用db.Delete(&User{}, 123)(内联主键)或db.Where("id = ?", 123).Delete(&User{}) - 批量删除更推荐
Where().Delete()形式,它拼接 WHERE 条件,不依赖 struct 实例的字段值
最容易被忽略的是:软删除是否生效,不只看字段名是不是 DeletedAt,而取决于类型是否为 gorm.DeletedAt、是否嵌入或加了 gorm:"index" 标签;而 Unscoped() 的透传性会让 Preload 关联也失效——这两个点在复杂查询和上线后排查问题时,往往卡住半天。











