gorm软删除本质是将deletedat设为非零时间而非执行delete语句;需用gorm.deletedat类型字段或嵌入gorm.model,查询默认过滤软删数据,须显式调用unscoped()才能查到。

软删除不是删数据,是改字段
GORM 的软删除本质就是把 DeletedAt 字段设为非零时间值,而不是执行 SQL 的 DELETE。它不依赖你手动加字段或写条件,只要结构体里有 gorm.DeletedAt 类型的 DeletedAt 字段,GORM 就自动启用软删除逻辑。
常见错误现象:删完查不到记录,但数据库里行还在;或者调 Delete() 后发现 DeletedAt 是空值、没生效——大概率是字段类型不对或没加 gorm.Model 嵌入。
-
DeletedAt必须是gorm.DeletedAt类型(即*time.Time),不能用time.Time或string - 推荐直接嵌入
gorm.Model,它已包含ID、CreatedAt、UpdatedAt和DeletedAt - 如果自己定义字段,必须显式加上
gorm:"index",否则Unscoped()以外的查询会忽略该行
type User struct {
gorm.Model // 自动带 DeletedAt
Name string
}
查不到软删数据?默认就过滤掉了
GORM 所有常规查询(Find、First、Where)默认跳过 DeletedAt IS NOT NULL 的记录。这不是 bug,是设计行为。想查含软删数据,必须显式调用 Unscoped()。
容易踩的坑:在管理后台“回收站”页面用 Find() 直接查,结果为空;或者做统计时漏掉 Unscoped(),导致总数少算。
立即学习“go语言免费学习笔记(深入)”;
-
Unscoped()是链式方法,要放在查询前,比如db.Unscoped().Where(...).Find(&users) -
Unscoped()影响整个链路,后续不能再靠其他条件“恢复”过滤,慎用 - 如果只想临时绕过软删除,又不想影响其他字段条件,可手动加
Where("deleted_at IS NULL")替代
Delete() 不触发硬删,除非加 Unscoped()
调 db.Delete(&user) 默认只是更新 DeletedAt,不会发 DELETE FROM 语句。真要物理删除,得组合 Unscoped()。
典型误用场景:迁移脚本里写 Delete() 想清空测试数据,结果越跑表越大;或者 API 接口文档写“删除用户”,前端以为删了,其实还能被 Unscoped() 拉回来。
- 软删:
db.Delete(&user)→ 更新DeletedAt - 硬删:
db.Unscoped().Delete(&user)→ 执行DELETE FROM - 批量硬删要小心:
db.Unscoped().Where("status = ?", "draft").Delete(&Post{}),没加Unscoped()还是软删
DeletedAt 字段别手动生成,也别乱改
DeletedAt 由 GORM 在 Delete() 时自动赋当前时间,你不该在代码里手动设 user.DeletedAt = time.Now() 或传零值进去。GORM 不会识别这种“手工软删”,后续查询仍可能命中。
更隐蔽的问题:用 Save() 更新带 DeletedAt 的结构体,可能意外覆盖掉原本的删除时间;或者用 Map 方式创建实例时漏掉该字段,导致插入时为 NULL 被当成未删除。
- 永远用
Delete()触发软删,不要靠Save()或构造器填DeletedAt - 如果需要自定义删除时间(比如回溯删除),得用
db.Session(&gorm.Session{NowFunc: func() time.Time { return yourTime }}).Delete(&user) - 迁移已有数据时,确保历史记录的
DeletedAt是NULL或有效时间,避免出现“半软删”状态
软删除真正麻烦的不是怎么写,而是所有人对“删”的理解是否一致——API、后台、DBA、审计日志,都得清楚 DeletedAt 不是装饰字段,而是一条隐式 WHERE 条件的开关。










