软删除字段必须是gorm.DeletedAt类型且命名为DeletedAt,否则GORM不启用软删;启用后Find/First等自动过滤已删记录,Delete执行UPDATE而非DELETE,Unscoped()可绕过软删。

软删除字段必须是 gorm.DeletedAt 类型
GORM 的软删除机制默认只识别名为 DeletedAt 且类型为 gorm.DeletedAt(即 *time.Time)的字段。如果你自定义字段名(比如 IsDeleted 或 DeletedTime),GORM 不会自动启用软删除逻辑,查询、更新、删除都会当作普通字段处理。
正确写法:
type User struct {
ID uint `gorm:"primaryKey"`
Name string
DeletedAt gorm.DeletedAt `gorm:"index"` // 必须叫 DeletedAt,且用 gorm.DeletedAt 类型
}
常见错误现象:
- 调用
db.Delete(&user)后数据库记录直接被物理删除(没走软删) -
db.First(&user, 1)查到已“删除”的记录,不自动过滤 - 手动加
WHERE deleted_at IS NULL才能查活跃数据 —— 说明软删没生效
启用软删除后,Find/First 默认自动过滤已删除记录
只要模型含合法的 DeletedAt 字段,GORM 会在所有读取操作(Find、First、Take、Count 等)自动追加 WHERE deleted_at IS NULL 条件。你不需要手动写 Unscoped() 以外的任何额外逻辑来“屏蔽”软删数据。
立即学习“go语言免费学习笔记(深入)”;
但要注意例外场景:
-
db.Unscoped().Find(&users):绕过软删,查全部(含已删) -
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users):专门查已删记录 - 关联查询(如
Preload)中,被预加载的子表若也有DeletedAt,同样受软删影响 —— 这是默认行为,不是 bug
性能提示:GORM 不会为 DeletedAt 字段自动建索引,高并发软删场景下建议显式加索引(如上面示例中的 gorm:"index")。
Delete 方法触发软删除,Unscoped().Delete 才是物理删除
对含 DeletedAt 字段的模型调用 db.Delete(),GORM 实际执行的是 UPDATE ... SET deleted_at = NOW(),而非 DELETE FROM。这是软删除的核心动作。
实操要点:
-
db.Delete(&user, 1)→ 软删(更新DeletedAt) -
db.Unscoped().Delete(&user, 1)→ 物理删(真正从表中移除) -
db.Unscoped().Where("id = ?", 1).Delete(&User{})→ 同样是物理删,注意Unscoped()要在Delete()前链式调用 - 如果想批量恢复(取消软删),用
db.Unscoped().Model(&User{}).Where("deleted_at IS NOT NULL").Update("deleted_at", nil)
容易踩的坑:在事务中混合使用 Unscoped() 和普通操作时,忘记 Unscoped() 作用域会延续到后续语句,导致意外物理删。
自定义软删除行为需重写 TableName 或用 SoftDelete 插件?不推荐
GORM 官方不支持更换软删除字段名或逻辑(比如用 is_deleted TINYINT)。强行通过 TableName 返回带条件的虚拟表、或自己实现钩子模拟软删,会导致 Count、Association、迁移命令(AutoMigrate)等行为异常,维护成本极高。
如果业务强依赖布尔型软删字段,务实做法是:
- 保留
DeletedAt gorm.DeletedAt字段用于 GORM 软删机制 - 额外加一个
IsDeleted bool字段,仅作业务层状态展示(用钩子同步值,或视图映射) - 避免让 ORM 层和业务层共用同一字段承担双重语义
复杂点在于:软删除不是开关,而是生命周期状态的一部分 —— 它影响查询、关联、计数、甚至外键约束行为。忽略这点,后期排查 Find 结果为空、Count 对不上、级联失效等问题会非常耗时。


















