如何在Golang中利用GORM实现软删除 Go语言DeletedAt字段逻辑

2次阅读

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

如何在Golang中利用GORM实现软删除 Go语言DeletedAt字段逻辑

软删除不是删数据,是改字段

GORM 的软删除本质就是把 DeletedAt 字段设为非零时间值,而不是执行 sqlDELETE。它不依赖你手动加字段或写条件,只要结构体里有 gorm.DeletedAt 类型的 DeletedAt 字段,GORM 就自动启用软删除逻辑。

常见错误现象:删完查不到记录,但数据库里行还在;或者调 Delete() 后发现 DeletedAt 是空值、没生效——大概率是字段类型不对或没加 gorm.Model 嵌入。

  • DeletedAt 必须是 gorm.DeletedAt 类型(即 *time.Time),不能用 time.TimeString
  • 推荐直接嵌入 gorm.Model,它已包含 IDCreatedAtUpdatedAtDeletedAt
  • 如果自己定义字段,必须显式加上 gorm:"index",否则 Unscoped() 以外的查询会忽略该行
type User struct {     gorm.Model // 自动带 DeletedAt     Name string }

查不到软删数据?默认就过滤掉了

GORM 所有常规查询(FindFirstWhere)默认跳过 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)
  • 迁移已有数据时,确保历史记录的 DeletedAtNULL 或有效时间,避免出现“半软删”状态

软删除真正麻烦的不是怎么写,而是所有人对“删”的理解是否一致——API、后台、dba、审计日志,都得清楚 DeletedAt 不是装饰字段,而是一条隐式 WHERE 条件的开关。

text=ZqhQzanResources