GORM 的 DeletedAt 仅实现逻辑隐藏而非回收站功能,需手动建回收站表、用 AfterDelete 钩子存快照、JSON 序列化原始数据,并通过独立方法区分活跃/已删/回收站态查询与还原。

Go 本身没有内置 ORM,也没有软删除标准实现;你得靠组合 gorm(最常用) + 显式字段设计 + 查询拦截来达成目标,否则容易误删或查不到“已删”数据。
为什么 GORM 的 DeletedAt 不是开箱即用的回收站
GORM 的 SoftDelete 模式(通过 gorm.DeletedAt 字段)默认只做「逻辑隐藏」:调用 Delete() 后记录被过滤出普通查询,但不会自动归档到独立回收站表,也无法按条件批量还原。
- 它不保存删除者、删除时间戳以外的元信息(如操作来源、原因)
-
Unscoped()能查到,但所有带Unscoped()的地方都得人工判断是否真要暴露“已删”数据 - 多个模型共用回收站时,
DeletedAt无法跨表统一管理生命周期(比如 30 天自动 purge)
手动建回收站表 + AfterDelete 钩子同步数据
真正可审计、可还原的回收站,需要独立表存储快照。GORM 的 AfterDelete 是唯一可靠时机——此时原记录还在事务中,能安全读取完整字段。
type User struct {
ID uint `gorm:"primaryKey"`
Name string
Email string
DeletedAt time.Time `gorm:"index"`
}
type RecycledItem struct {
ID uint `gorm:"primaryKey"`
TableName string `gorm:"index"` // "users", "orders"
RecordID uint `gorm:"index"` // 对应原表主键
Payload []byte `gorm:"type:jsonb"` // PostgreSQL,或 longtext(MySQL)
DeletedBy uint // 可选:操作人 ID
DeletedAt time.Time `gorm:"index"`
}
func (u *User) AfterDelete(tx *gorm.DB) error {
item := RecycledItem{
TableName: "users",
RecordID: u.ID,
Payload: toJSON(u), // 自行实现 json.Marshal,排除敏感字段
DeletedAt: time.Now(),
}
return tx.Create(&item).Error
}
- 别在
BeforeDelete里读*u,此时字段可能已被清空(尤其用了Select()限制字段时) -
Payload用 JSON 存,避免回收站表结构随原模型频繁变更 - 如果用 MySQL,把
type:jsonb换成type:longtext,并确保sql_mode允许 JSON 函数
查询时区分「活跃态」「已删态」「回收站态」
不要依赖 Unscoped() 混查,而是明确用不同方法封装语义:
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
FindActiveUser():默认加WHERE deleted_at IS NULL -
FindDeletedUser():查RecycledItem表 +JOIN users ON ... WHERE users.deleted_at IS NOT NULL(用于还原前预览) -
RestoreUser(id):先查RecycledItem得到Payload,反序列化后Create()新记录,并更新原记录DeletedAt = nil(注意主键冲突)
关键点:还原不是简单 UPDATE users SET deleted_at = NULL,因为原记录的 DeletedAt 值已丢失业务上下文(比如用户改过邮箱),必须从回收站 JSON 中恢复原始状态。
软删除字段命名和迁移要提前约定
别用 is_deleted 这种布尔字段——它无法支持「删除时间」排序、「软删后更新」等需求。GORM 官方推荐且唯一兼容 SoftDelete 的字段是 DeletedAt *time.Time。
- 建表迁移时,必须显式添加:
db.Migrator().AddColumn(&User{}, "DeletedAt") - 已有表加该字段后,需执行
UPDATE users SET deleted_at = NULL,否则 GORM 会把所有旧记录当「已删」 - 如果用其他字段名(如
RemovedAt),必须全局配置:gorm.Config{NowFunc: func() time.Time { return time.Now() }}, then use field tag <code>gorm:"column:removed_at"—— 但此时Delete()和Unscoped()行为不再自动生效,得自己写 scope
回收站逻辑越往后补,越容易漏掉钩子或查不到上下文;最好在第一个需要软删的模型上线前,就把 RecycledItem 表和基础钩子跑通。

















