掌握 GORM 删除:单条删除、批量删除与软删除实践

GORM 的删除操作是数据库交互中的重要环节,合理使用删除功能对数据管理和应用性能至关重要。本文将系统讲解 GORM 中常用的删除方法,重点剖析单条删除、批量删除和软删除等核心功能,并结合实战场景演示高级删除技巧,帮助你全面掌握 GORM 删除操作的最佳实践。

一、单条记录删除:精准定位与条件筛选

1.1 按主键删除记录

按主键删除是最常用的删除方式,GORM 提供了简洁的 API 实现:

sql 复制代码
// 场景1:删除指定主键的记录
var email Email
email.ID = 10  // 假设Email的ID是10
db.Delete(&email)
// 生成SQL: DELETE FROM emails WHERE id = 10;

// 场景2:通过内联条件删除(主键为数字)
db.Delete(&User{}, 10)
// 生成SQL: DELETE FROM users WHERE id = 10;

// 场景3:主键为字符串的删除(如UUID)
db.Delete(&User{}, "1b74413f-f3b8-409f-ac47-e8c062e3472a")
// 生成SQL: DELETE FROM users WHERE id = "1b74413f-f3b8-409f-ac47-e8c062e3472a";

// 场景4:批量主键删除(切片形式)
db.Delete(&users, []int{1, 2, 3})
// 生成SQL: DELETE FROM users WHERE id IN (1, 2, 3);

核心要点

  • 删除操作需要指定主键,否则会触发批量删除
  • 支持数字和字符串类型的主键
  • 内联条件方式删除更加简洁,无需先查询记录

1.2 带条件的单条删除

在实际业务中,经常需要根据条件删除记录,而非仅依赖主键:

sql 复制代码
// 基础条件删除
var email Email
email.ID = 10  // 设置主键以便定位记录
db.Where("name = ?", "重要邮件").Delete(&email)
// 生成SQL: DELETE FROM emails WHERE id = 10 AND name = "重要邮件";

// 组合条件删除
db.Model(&User{}).Where("age > ?", 60).Where("status = ?", "inactive").Delete(&User{})
// 生成SQL: DELETE FROM users WHERE age > 60 AND status = "inactive";

// 内联条件删除(更简洁的写法)
db.Delete(&User{}, "age > ? AND status = ?", 60, "inactive")
// 生成SQL: DELETE FROM users WHERE age > 60 AND status = "inactive";

最佳实践

  • 删除前尽量通过主键定位,确保仅删除目标记录
  • 复杂条件删除时,先通过查询验证条件范围,再执行删除
  • 敏感数据删除前考虑添加确认机制

二、批量删除:高效处理数据集

2.1 条件批量删除

当需要删除符合特定条件的多条记录时,批量删除是更高效的选择:

sql 复制代码
// 场景1:按条件删除所有匹配记录
db.Where("email LIKE ?", "%spam%").Delete(&Email{})
// 生成SQL: DELETE FROM emails WHERE email LIKE "%spam%";

// 场景2:结合子查询的批量删除
subQuery := db.Table("users").Select("id").Where("status = ?", "blocked")
db.Delete(&Order{}, "user_id IN (?)", subQuery)
// 生成SQL: DELETE FROM orders WHERE user_id IN (SELECT id FROM users WHERE status = "blocked");

// 场景3:删除指定时间范围内的记录
db.Where("created_at < ?", time.Now().AddDate(0, -1, 0)).Delete(&Log{})
// 生成SQL: DELETE FROM logs WHERE created_at < "2023-06-01";

2.2 主键切片批量删除

通过主键切片删除多条记录,比循环单条删除更高效:

less 复制代码
// 场景1:删除多个主键的记录
var users = []User{{ID: 1}, {ID: 2}, {ID: 3}}
db.Delete(&users)
// 生成SQL: DELETE FROM users WHERE id IN (1, 2, 3);

// 场景2:主键切片结合条件删除
var userIDs = []int64{10, 11, 12}
db.Delete(&User{}, "id IN (?) AND status = ?", userIDs, "inactive")
// 生成SQL: DELETE FROM users WHERE id IN (10, 11, 12) AND status = "inactive";

// 场景3:高效删除大量记录(分批处理)
var userIDs []int64
// 先查询需要删除的ID列表
db.Model(&User{}).Where("created_at < ?", oldTime).Pluck("id", &userIDs)

// 分批删除,避免一次性删除过多记录
for i := 0; i < len(userIDs); i += 100 {
    end := i + 100
    if end > len(userIDs) {
        end = len(userIDs)
    }
    db.Delete(&User{}, "id IN (?)", userIDs[i:end])
}

2.3 阻止全局删除:安全第一

GORM 严格限制无条件的批量删除,避免误操作导致数据丢失:

less 复制代码
// 危险操作:无条件删除会报错
db.Delete(&User{}).Error // 抛出 gorm.ErrMissingWhereClause

// 安全做法1:添加有效条件
db.Where("created_at < ?", time.Now().AddDate(-1, 0, 0)).Delete(&User{})

// 安全做法2:使用原生SQL(明确知晓风险)
db.Exec("DELETE FROM users WHERE status = 'temp'")

// 安全做法3:启用全局更新模式(不推荐,仅临时使用)
db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})

安全规范

  • 永远为删除操作添加具体条件,避免 WHERE 1=1 等宽泛条件
  • 生产环境禁用 AllowGlobalUpdate,通过代码逻辑保证条件正确性
  • 重要删除操作前先通过 Find 验证条件范围,确认无误后再执行删除

三、软删除:GORM 核心特性与实践

3.1 软删除基础用法

软删除是 GORM 的重要特性,通过标记删除而非物理删除记录,保留数据可恢复性:

sql 复制代码
// 模型定义(包含DeletedAt字段)
type User struct {
  ID        uint
  Name      string
  Email     string
  CreatedAt time.Time
  UpdatedAt time.Time
  DeletedAt gorm.DeletedAt // 软删除字段,自动添加
}

// 场景1:软删除单条记录
var user User
db.First(&user, 1)
db.Delete(&user)
// 生成SQL: UPDATE users SET deleted_at="2023-07-01 10:00:00" WHERE id = 1;

// 场景2:批量软删除
db.Where("age > ?", 60).Delete(&User{})
// 生成SQL: UPDATE users SET deleted_at="2023-07-01 10:00:00" WHERE age > 60;

// 场景3:软删除后查询(默认不返回软删除记录)
db.Where("age = 20").Find(&users)
// 生成SQL: SELECT * FROM users WHERE age = 20 AND deleted_at IS NULL;

实现原理

  • 当模型包含 gorm.DeletedAt 字段时,自动启用软删除
  • Delete 操作变为更新 deleted_at 字段,而非物理删除
  • 常规查询会自动添加 deleted_at IS NULL 条件,忽略软删除记录

3.2 软删除高级操作

3.2.1 查询软删除记录

需要查询被软删除的记录时,使用 Unscoped 模式:

sql 复制代码
// 查询所有软删除的记录
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&users)
// 生成SQL: SELECT * FROM users WHERE deleted_at IS NOT NULL;

// 查询特定软删除记录
db.Unscoped().First(&user, 1)
// 生成SQL: SELECT * FROM users WHERE id = 1; // 包含软删除记录

// 混合查询(包括正常和软删除记录)
db.Unscoped().Where("age > ?", 30).Find(&users)
// 生成SQL: SELECT * FROM users WHERE age > 30;

3.2.2 永久删除记录

某些场景需要物理删除记录,使用 Unscoped 模式:

scss 复制代码
// 永久删除单条记录
var user User
user.ID = 1
db.Unscoped().Delete(&user)
// 生成SQL: DELETE FROM users WHERE id = 1;

// 永久删除符合条件的记录
db.Unscoped().Where("created_at < ?", time.Now().AddDate(-2, 0, 0)).Delete(&Log{})
// 生成SQL: DELETE FROM logs WHERE created_at < "2021-07-01";

// 结合软删除的永久删除(先查询软删除记录再永久删除)
var softDeletedUsers []User
db.Unscoped().Where("deleted_at < ?", time.Now().AddDate(-1, 0, 0)).Find(&softDeletedUsers)
db.Unscoped().Delete(&softDeletedUsers)

3.2.3 自定义软删除标志

GORM 支持自定义软删除的实现方式,通过插件可以使用不同的数据类型:

go 复制代码
// 使用插件实现不同软删除标志
import "gorm.io/plugin/soft_delete"

// 场景1:Unix时间戳作为删除标志
type User struct {
  ID        uint
  Name      string
  DeletedAt soft_delete.DeletedAt
}
// 生成SQL: UPDATE users SET deleted_at = 1688275200 WHERE id = 1;

// 场景2:使用1/0标志位
type User struct {
  ID    uint
  Name  string
  IsDel soft_delete.DeletedAt `gorm:"softDelete:flag"`
}
// 生成SQL: UPDATE users SET is_del = 1 WHERE id = 1;

// 场景3:混合模式(标志位+删除时间)
type User struct {
  ID        uint
  Name      string
  DeletedAt time.Time
  IsDel     soft_delete.DeletedAt `gorm:"softDelete:flag,DeletedAtField:DeletedAt"`
}
// 生成SQL: UPDATE users SET is_del = 1, deleted_at = "2023-07-01 10:00:00" WHERE id = 1;

四、高级删除技巧与钩子函数

4.1 删除钩子函数:业务逻辑注入

GORM 提供删除相关的钩子函数,用于实现数据验证、日志记录等功能:

go 复制代码
type User struct {
  ID   uint
  Name string
  Role string
}

// BeforeDelete 钩子:删除前验证
func (u *User) BeforeDelete(tx *gorm.DB) error {
  // 禁止删除管理员用户
  if u.Role == "admin" {
    return errors.New("管理员用户禁止删除")
  }
  
  // 记录删除日志
  logService.RecordDelete(u.ID, "user")
  return nil
}

// AfterDelete 钩子:删除后清理关联数据
func (u *User) AfterDelete(tx *gorm.DB) error {
  // 删除用户关联的所有订单
  return tx.Where("user_id = ?", u.ID).Delete(&Order{}).Error
}

// 使用钩子示例
db.Delete(&user)
// 会先触发BeforeDelete,再执行删除,最后触发AfterDelete

常用钩子函数

  • BeforeDelete:删除前执行,可用于数据验证、权限检查
  • AfterDelete:删除后执行,可用于清理关联数据、记录日志
  • 钩子函数中可以访问事务对象 tx,进行额外的数据库操作

4.2 返回删除行数据:数据库回写支持

某些数据库支持删除时返回被删除的数据,GORM 提供了相应的支持:

sql 复制代码
// 场景1:返回所有被删除的列
var users []User
db.Clauses(clause.Returning{}).Where("role = ?", "admin").Delete(&users)
// 生成SQL: DELETE FROM users WHERE role = "admin" RETURNING *
// users 变量会填充被删除的记录数据

// 场景2:返回指定列
db.Clauses(clause.Returning{Columns: []clause.Column{{Name: "name"}, {Name: "email"}}}).Delete(&users)
// 生成SQL: DELETE FROM users WHERE id IN (1,2,3) RETURNING name, email
// users 变量会填充name和email字段,其他字段为零值

// 场景3:结合事务使用
db.Transaction(func(tx *gorm.DB) error {
  var deletedUsers []User
  if err := tx.Clauses(clause.Returning{}).Where("status = ?", "temp").Delete(&deletedUsers).Error; err != nil {
    return err
  }
  
  // 处理被删除的用户数据
  for _, user := range deletedUsers {
    auditService.RecordDeletion(user)
  }
  
  return nil
})

支持的数据库

  • PostgreSQL 完全支持 RETURNING 子句
  • MySQL 8.0+ 支持 RETURNING,但需要显式启用
  • SQLite 不支持返回删除数据

五、删除操作最佳实践

5.1 方法选择策略

场景需求 推荐方法 示例代码
单条记录删除(已知主键) Delete(&model) db.Delete(&user)
单条记录条件删除 Delete(&model, condition) db.Delete(&user, "status = ?", "inactive")
批量条件删除 Where+Delete db.Where("age > ?", 60).Delete(&User{})
批量主键删除 Delete(&models) db.Delete(&users)
软删除 包含 DeletedAt 字段 type User struct { ... DeletedAt gorm.DeletedAt ... }
永久删除 Unscoped+Delete db.Unscoped().Delete(&user)

5.2 性能优化要点

  1. 避免循环删除

    • 用批量删除替代循环单条删除,减少数据库交互
    • 主键切片删除比条件批量删除更高效(数据库索引友好)
  2. 大数据集处理

    • 对于大量数据删除,先查询 ID 列表,再分批删除
    • 使用 FindInBatches 结合删除,避免内存溢出
  3. 软删除性能考虑

    • 软删除本质是更新操作,比物理删除开销稍大
    • 定期清理历史软删除记录,保持表结构简洁

5.3 数据安全规范

  • 删除前验证:重要删除操作前,先通过查询确认影响范围
  • 软删除优先:非必要不物理删除,使用软删除保留数据可恢复性
  • 操作审计:记录所有删除操作,包括操作人员、时间和条件
  • 备份机制:生产环境删除前确保有最新数据备份

通过掌握这些删除技巧,你可以在 GORM 中高效、安全地管理数据删除操作,同时保持数据的完整性和可恢复性。建议在实际项目中根据业务场景选择合适的删除方法,并通过单元测试验证删除逻辑的正确性。

如果这篇文章对大家有帮助可以点赞关注,你的支持就是我的动力😊!

相关推荐
hello 早上好36 分钟前
MyBatis 动态 SQL、#{}与 ${}区别、与 Hibernate区别、延迟加载、优势、XML映射关系
sql·mybatis·hibernate
拉巴力不吃三文鱼3 小时前
SaturnCLI-Go:基于本地 Socket 的高效任务调度通信组件
go
NullPointerExpection5 小时前
LLM大语言模型不适合统计算数,可以让大模型根据数据自己建表、插入数据、编写查询sql统计
数据库·人工智能·sql·算法·llm·llama·工作流
我命由我123457 小时前
Spring Boot - Spring Boot 集成 MyBatis 分页实现 手写 SQL 分页
java·spring boot·后端·sql·spring·java-ee·mybatis
切糕师学AI7 小时前
SQL中对字符串字段模糊查询(LIKE)的索引命中情况
数据库·sql
茅坑的小石头7 小时前
SQL,在join中,on和where的区别
sql
云边散步8 小时前
🧱 第1篇:什么是SQL?数据库是啥?我能吃吗?
数据库·sql
vv安的浅唱8 小时前
Golang基础笔记三之数组和切片
后端·go
程序员爱钓鱼8 小时前
Go语言100个实战案例 - 找出切片中的最大值与最小值
后端·google·go