GORM软删除必须用*time.Time类型的DeletedAt字段并嵌入gorm.Model或加gorm:"index"标签,否则Delete()执行物理删除;默认查询自动过滤软删记录,查全部需Unscoped()前置。
必须用 *time.time 类型的 deletedat 字段,且模型需嵌入 gorm.model 或显式声明该字段并加 gorm:"index" 标签,否则 delete() 仍会执行物理删除。
为什么删完查不到记录?默认就过滤了
GORM 所有常规查询(Find、First、Where)都会自动追加 WHERE deleted_at IS NULL。这不是 bug,是软删除机制的默认行为。
- 想查“存活”数据:直接用
db.Where("name = ?", "foo").Find(&users)✅ - 想查包括已软删的全部数据:必须前置
Unscoped(),如db.Unscoped().Where("name = ?", "foo").Find(&users)✅ - 只查已软删的记录:
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)✅ -
Unscoped()是链式方法,必须放在最前面;它会影响整个调用链,包括后续的Preload关联查询
DeletedAt 字段类型写错,新记录就直接不可见
错误写法:DeletedAt time.Time —— 零值 0001-01-01 00:00:00 +0000 UTC 会被 GORM 判定为“已删除”,导致刚 Create 的记录在普通查询中永远查不到。
- 正确写法一(推荐):
gorm.Model已内置DeletedAt gorm.DeletedAt(即*time.Time),直接嵌入即可 - 正确写法二(自定义):
DeletedAt *time.Time `gorm:"index"` - 不加
gorm:"index"不影响功能,但高并发或大数据量时,WHERE deleted_at IS NULL查询可能变慢
恢复、物理删除、批量操作都得靠 Unscoped()
Delete() 默认只是更新 DeletedAt,真要彻底清除或还原,必须绕过软删除拦截层。
- 恢复一条软删记录:
db.Unscoped().Model(&user).Update("deleted_at", nil)(注意是nil,不是time.Time{}) - 物理删除单条:
db.Unscoped().Delete(&user) - 批量物理删除:
db.Unscoped().Where("status = ?", "draft").Delete(&Post{}) - 别在业务主流程里滥用
Unscoped():比如用户列表页不该显示已删内容;它更适合回收站、审计日志等明确需要“绕过过滤”的场景
最容易被忽略的是:软删除生效不只看字段名,更依赖字段类型和标签;而 Unscoped() 的透传性会让关联查询也跳过过滤——这点在带 Preload 的接口里极易出错。


















