软删除

软删除是一种"假删除"机制。记录不会真正从数据库消失,而是被标记为已删除。这在业务系统中很常见,支持数据恢复和审计追溯。

启用软删除

模型嵌入 gorm.DeletedAt 字段:

type User struct {
    ID        uint
    Name      string
    DeletedAt gorm.DeletedAt
}

或使用 gorm.Model

type User struct {
    gorm.Model
    Name string
}

gorm.Model 定义:

type Model struct {
    ID        uint           `gorm:"primaryKey"`
    CreatedAt time.Time
    UpdatedAt time.Time
    DeletedAt gorm.DeletedAt `gorm:"index"`
}

软删除行为

删除记录:

db.Delete(&User{}, 1)

生成的 SQL:

UPDATE users SET deleted_at = '2024-01-15 10:30:00' WHERE id = 1 AND deleted_at IS NULL

记录仍然存在,只是 deleted_at 被设置为当前时间。

查询自动过滤

普通查询会自动排除软删除记录:

var users []User
db.Find(&users)

生成的 SQL:

SELECT * FROM users WHERE deleted_at IS NULL

查询单条同样过滤:

var user User
db.First(&user, 1)

如果记录被软删除,会返回 ErrRecordNotFound

包含软删除记录

Unscoped 查询所有记录:

db.Unscoped().Find(&users)

生成的 SQL:

SELECT * FROM users

查询单条:

var user User
db.Unscoped().First(&user, 1)

只查询软删除记录

db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)

恢复软删除

db.Unscoped().Model(&User{}).Where("id = ?", 1).Update("deleted_at", nil)

或用 Unscoped().Update

var user User
db.Unscoped().First(&user, 1)
user.DeletedAt = gorm.DeletedAt{}
db.Save(&user)

真正删除

Unscoped().Delete 永久删除:

db.Unscoped().Delete(&User{}, 1)

生成的 SQL:

DELETE FROM users WHERE id = 1

批量软删除

db.Where("age < ?", 18).Delete(&User{})

批量恢复:

db.Unscoped().Model(&User{}).Where("age < ?", 18).Update("deleted_at", nil)

软删除与关联

软删除会影响关联查询:

type User struct {
    ID      uint
    Name    string
    Orders  []Order
}

type Order struct {
    ID     uint
    UserID uint
    Amount float64
}

var user User
db.Preload("Orders").First(&user, 1)

如果 Order 也有软删除,查询时会自动过滤。

关联记录被软删除后,外键关系仍然存在,只是查询时看不到。

软删除的索引

deleted_at 字段应该加索引:

type User struct {
    gorm.Model
    Name string `gorm:"index"`
}

gorm.Model 已经为 DeletedAt 添加了索引。

复合唯一索引要包含 deleted_at

type User struct {
    gorm.Model
    Email string `gorm:"uniqueIndex:idx_email_deleted"`
}

这样同一个邮箱可以"删除"后重新注册。

自定义删除时间字段

不用 gorm.DeletedAt,自定义字段:

type User struct {
    ID        uint
    Name      string
    DeletedAt *time.Time `gorm:"index"`
}

func (User) TableName() string {
    return "users"
}

GORM 会识别名为 DeletedAt 的字段。

字段名不同时:

type User struct {
    ID        uint
    Name      string
    IsDeleted bool `gorm:"default:false"`
}

需要自定义 Scope 实现,不推荐。

软删除的利弊

优点

  • 数据可恢复,误删不怕
  • 支持审计追溯
  • 保持数据完整性
  • 实现简单

缺点

  • 表数据膨胀
  • 需要定期清理
  • 查询性能受影响
  • 唯一索引需要特殊处理

定期清理

软删除数据积累过多,需要定期清理:

func CleanSoftDeleted(db *gorm.DB, before time.Time) error {
    return db.Unscoped().
        Where("deleted_at IS NOT NULL AND deleted_at < ?", before).
        Delete(&User{}).
        Error
}

CleanSoftDeleted(db, time.Now().AddDate(-1, 0, 0))

删除一年前软删除的记录。

软删除与审计

软删除天然支持审计,但可以配合审计日志:

type User struct {
    gorm.Model
    Name      string
    DeletedBy uint
}

func (u *User) BeforeDelete(tx *gorm.DB) error {
    userID, ok := tx.Statement.Context.Value("user_id").(uint)
    if ok {
        u.DeletedBy = userID
    }
    return nil
}

记录谁删除了数据。

常见问题

唯一约束冲突

软删除后重新创建相同记录:

db.Create(&User{Name: "张三"})
db.Delete(&User{}, 1)
db.Create(&User{Name: "张三"})

如果 name 有唯一约束,会冲突。

解决方案:唯一索引包含 deleted_at

查询忘记 Unscoped

需要查所有记录时忘记 Unscoped

db.Find(&users)

只查到未删除的记录,可能不符合预期。

统计数量

var count int64
db.Model(&User{}).Count(&count)

只统计未删除的记录。要统计所有:

db.Unscoped().Model(&User{}).Count(&count)

小结

软删除是业务系统的常用功能,GORM 的实现简单优雅。注意唯一索引、数据清理、查询范围这些问题,用好软删除能提升系统的健壮性。第四部分 CRUD 操作到此结束。