软删除在gorm中需deletedat为*time.time类型并加gorm:"index"标签,否则delete()执行物理删除;普通查询自动过滤软删记录,查已删或恢复必须用unscoped(),且其须置于链式调用最前。

软删除在 GORM 中不会自动生效,必须结构体含 gorm.DeletedAt 类型字段且加 gorm:"index" 标签,否则 Delete() 仍是物理删除。
为什么 Delete() 没触发软删除?
最常见原因是字段类型或定义不合规:
-
DeletedAt必须是*time.Time(指针),写成time.Time会导致新插入记录的零值被误判为“已删除”,查不到 - 没嵌入
gorm.Model,也没显式声明DeletedAt gorm.DeletedAt `gorm:"index"` - 字段名不是
DeletedAt(GORM 不识别is_deleted、removed_at等别名) - 建表时未启用
gorm:"index",高并发下 WHERE 过滤可能漏行或变慢
如何让普通查询自动过滤软删记录?
这是 GORM 的默认行为,但依赖两个前提:模型正确声明了 DeletedAt 字段,且你没调用 Unscoped():
-
db.First(&u, 1)、db.Where("name = ?", "foo").Find(&users)都会自动加WHERE deleted_at IS NULL - 不需要手动写
Where("deleted_at IS NULL"),重复加可能导致 SQL 错误或语义混乱 - 如果用了
Preload("Orders"),关联表也受过滤影响——除非主表或关联表模型本身没配DeletedAt
怎么查已软删除的记录或恢复它们?
必须显式使用 Unscoped(),它不是可选装饰,而是绕过软删除逻辑的唯一入口:
- 查全部(含已删):
db.Unscoped().Where("name LIKE ?", "%test%").Find(&users) - 只查已删:
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)(注意字段名小写,数据库实际列名是deleted_at) - 恢复单条:
db.Unscoped().Model(&user).Where("id = ?", 123).Update("deleted_at", nil)—— 第二个参数必须是nil,不是time.Time{} - 批量恢复:
db.Unscoped().Model(&User{}).Where("status = ?", "archived").Update("deleted_at", nil)
物理删除和统计总数容易踩哪些坑?
硬删和统计都绕不开 Unscoped(),但它的作用范围比你想的更广:
-
db.Unscoped().Delete(&user)会跳过BeforeDelete钩子,也不检查外键约束,慎用于生产 -
Count()默认不统计软删记录;要算“总注册数”,必须写db.Unscoped().Model(&User{}).Count(&n) -
Unscoped()是链式方法,必须放在最前:db.Unscoped().Where(...).Find()✅,db.Where(...).Unscoped().Find()❌(无效) - 事务中恢复+更新要原子执行,否则其他协程可能读到
DeletedAt != nil但数据已部分更新的中间态
真正难的不是加字段,而是理解 Unscoped() 的透传性——它一旦出现,就会影响整个查询链,包括 Preload 和后续所有条件。很多线上 bug 都源于某处多加了一个点号,或忘了它对关联表的副作用。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











