Golang实现数据库软删除方法
应使用deleted_at(time.Time类型),因GORM官方软删除机制依赖DeletedAt字段名和time.Time类型,is_deleted会绕过自动过滤、更新及Unscoped()等核心行为。
应使用 deleted_at(time.Time 类型),因 GORM 官方软删除机制依赖 DeletedAt 字段名和 time.Time 类型,is_deleted 会绕过自动过滤、更新及 Unscoped() 等核心行为。

软删除字段该用 deleted_at 还是 is_deleted?
Go 生态中,gorm 官方软删除约定是 deleted_at(*time.Time 类型),不是布尔字段。用 is_deleted 会绕过 GORM 的自动软删除机制,导致 Find、Update、Delete 行为不一致,且无法利用 Unscoped() 恢复逻辑。
必须满足两个条件才能启用 GORM 软删除:
struct中定义字段名恰好为DeletedAt(首字母大写)且类型为*time.Time- 结构体标签里显式声明
gorm:"index"(非必需但强烈建议,否则WHERE deleted_at IS NULL查询无索引)
示例:
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"not null"`
DeletedAt *time.Time `gorm:"index"`
}
GORM 的 Delete 为什么没删数据?
调用 db.Delete(&user) 实际执行的是 UPDATE SET deleted_at = NOW(),不是 DELETE FROM。这是软删除的默认行为,无需额外配置。
但要注意几个典型陷阱:
- 如果
user是零值(比如刚 new 出来没赋 ID),GORM 会报invalid argument错误 —— 必须传入带有效主键的实例 - 若想物理删除(跳过软删除),必须显式调用
Unscoped().Delete() db.Where("name = ?", "foo").Delete(&User{})会批量软删除所有匹配记录,但不会触发单条记录的BeforeDelete钩子
查询时如何自动过滤已软删除的记录?
GORM 默认所有查询(Find、First、Where 等)都会自动加上 WHERE deleted_at IS NULL 条件 —— 前提是模型含 DeletedAt 字段且未调用 Unscoped()。
常见误操作:
- 手动写
Where("deleted_at IS NULL"):多余,且可能和 GORM 自动添加的条件冲突(尤其在复杂嵌套查询中) - 用
Raw()执行原生 SQL:完全绕过软删除逻辑,查到已软删除的数据 —— 若需安全查询,务必自己补AND deleted_at IS NULL - 关联查询(
Preload)默认也受软删除影响,但若主表用了Unscoped(),关联表仍走默认过滤,不会“传染”
需要恢复软删除数据时,别直接改 deleted_at
最安全的方式是调用 Unscoped().Model(&user).Update("deleted_at", nil),而不是 db.Model(&user).Update("deleted_at", nil) —— 后者会被 GORM 当作普通字段更新,但因软删除机制存在,Update 默认仍加 WHERE deleted_at IS NULL,结果是找不到记录,更新失败。
另外注意:
nil是唯一能“恢复”的值,设成任意时间(包括过去时间)都算已删除- 恢复操作本身不会触发
AfterDelete或BeforeDelete钩子,因为这不是一次删除行为 - 如果业务要求“恢复时校验状态”,得在
Update前手动查一遍deleted_at != nil
软删除真正难的不是实现,而是团队对“何时该 Unscoped、何时不该、哪些接口暴露了 raw 查询”缺乏统一约束。字段命名、钩子遗漏、索引缺失、测试覆盖不足——这些地方出问题,比写错一行 Delete 更难排查。


































