如何用GORM在Go中实现软删除,将DeletedAt字段逻辑巧妙改写?

2026-04-29 00:422阅读0评论SEO资源
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计995个文字,预计阅读时间需要4分钟。

如何用GORM在Go中实现软删除,将DeletedAt字段逻辑巧妙改写?

GORM的软删除本质是将DeletedAt字段设置为非零时间值,而不是执行DELETE SQL语句。它不依赖于手动添加字段或编写条件,只需在结构体中包含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 条件的开关。

本文共计995个文字,预计阅读时间需要4分钟。

如何用GORM在Go中实现软删除,将DeletedAt字段逻辑巧妙改写?

GORM的软删除本质是将DeletedAt字段设置为非零时间值,而不是执行DELETE SQL语句。它不依赖于手动添加字段或编写条件,只需在结构体中包含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 条件的开关。