软删除是一种"假删除"机制。记录不会真正从数据库消失,而是被标记为已删除。这在业务系统中很常见,支持数据恢复和审计追溯。
模型嵌入 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 操作到此结束。