3. CRUD 操作:Create、First/Find/Take、Updates/Save、Delete 与软删除
GORM 的 CRUD 操作是构建 Web 服务数据层的基石。本章深入剖析每个操作的执行语义、边界条件与常见陷阱,帮助你在实际项目中精准控制数据流。
3.1 Create:插入与批量插入
Create 方法用于向数据库插入一条或多条记录。GORM 在插入时会自动处理主键、时间戳和关联字段。
单条记录创建
user := User{Name: "Alice", Age: 30}
result := db.Create(&user)
// 插入后 user.ID 会被自动回填
fmt.Println(user.ID) // 1
fmt.Println(result.Error) // nil
fmt.Println(result.RowsAffected) // 1
批量插入
传递切片即可实现批量插入,GORM 会合并为一条 SQL 语句执行,大幅提升性能。
users := []User{
{Name: "Bob", Age: 25},
{Name: "Carol", Age: 28},
{Name: "Dave", Age: 32},
}
result := db.Create(&users)
fmt.Println(len(users)) // 3
fmt.Println(result.RowsAffected) // 3
性能提示:批量插入时,GORM 默认每 100 条记录拆分一次 SQL。可通过
db.CreateInBatches(&users, 50) 自定义批次大小。
指定字段插入
使用 Select 或 Omit 控制插入字段,避免零值或敏感字段被意外写入。
// 仅插入 Name 和 Age 字段
db.Select("Name", "Age").Create(&user)
// 跳过 Password 字段
db.Omit("Password").Create(&user)
3.2 查询:First / Find / Take
GORM 提供三种主要查询方法,各自适用于不同场景。理解它们的差异是避免“记录未找到”异常的关键。
| 方法 | 行为 | 未找到时 | 典型用途 |
|---|---|---|---|
First |
按主键升序取第一条 | 返回 ErrRecordNotFound |
获取单条记录,期望必须存在 |
Take |
不排序取第一条 | 返回 ErrRecordNotFound |
获取任意一条记录 |
Find |
返回所有匹配记录 | 返回空切片,不报错 | 列表查询、条件筛选 |
// First:按主键升序
var user User
err := db.First(&user, 10).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
// 处理未找到
}
// Take:不排序
db.Take(&user, "name = ?", "Alice")
// Find:返回切片,未找到时为空
var users []User
db.Where("age > ?", 20).Find(&users)
// users 为 [],不会返回错误
常见错误:使用
First 或 Take 时未检查 ErrRecordNotFound,直接访问返回的结构体字段会导致 panic。建议始终使用 errors.Is(err, gorm.ErrRecordNotFound) 进行判断。
3.3 Updates / Save:更新策略
GORM 提供两种更新方式:Save 全字段更新,Updates 选择性更新。选择错误可能导致零值覆盖或性能问题。
Save:全字段保存
Save 会更新所有字段,包括零值。适用于从数据库查询后修改再写回的场景。
// 先查询
var user User
db.First(&user, 1)
// 修改字段
user.Name = "Alice Updated"
user.Age = 0 // 零值
// Save 会更新所有字段,包括 Age=0
db.Save(&user)
// UPDATE users SET name='Alice Updated', age=0, ... WHERE id=1
Updates:选择性更新
Updates 只更新非零值字段(除非使用 Select 指定)。推荐用于部分更新 API。
// 使用结构体:零值字段会被忽略
db.Model(&user).Updates(User{Name: "Bob", Age: 0})
// 只更新 name,age 不变
// 使用 map:零值也会被更新
db.Model(&user).Updates(map[string]interface{}{
"name": "Bob",
"age": 0,
})
// name 和 age 都会被更新
最佳实践:RESTful API 的 PATCH 请求推荐使用
Updates + map,避免结构体零值问题。PUT 请求可使用 Save 或 Updates + Select 全字段更新。
3.4 Delete 与软删除
GORM 默认使用软删除:当模型包含 DeletedAt 字段时,Delete 不会真正删除记录,而是设置删除时间戳。
软删除模型定义
import "gorm.io/plugin/soft_delete"
type User struct {
ID uint
Name string
DeletedAt soft_delete.DeletedAt `gorm:"index"`
}
软删除操作
// 软删除:设置 deleted_at 时间戳
db.Delete(&user, 1)
// UPDATE users SET deleted_at='2024-01-15 10:00:00' WHERE id=1
// 查询时自动忽略软删除记录
var users []User
db.Find(&users)
// SELECT * FROM users WHERE deleted_at IS NULL
// 查询包含软删除记录
db.Unscoped().Find(&users)
// SELECT * FROM users
物理删除
使用 Unscoped 可永久删除记录。
// 物理删除
db.Unscoped().Delete(&user, 1)
// DELETE FROM users WHERE id=1
事务边界注意:软删除字段
deleted_at 是唯一索引的一部分时,重复软删除再创建相同唯一键的记录会导致冲突。建议唯一索引组合 deleted_at 或使用唯一键 + 软删除时间戳的复合索引。
3.5 CRUD 操作流程总览
3.6 常见陷阱与避坑指南
- 零值覆盖:使用结构体进行
Updates时,零值字段(0、""、false)不会被更新。如需更新零值,使用map[string]interface{}或Select显式指定。 - 软删除唯一索引冲突:软删除记录仍占用唯一索引。解决方案:组合唯一索引包含
deleted_at字段,或使用gorm.DeletedAt的uniq标签。 - 批量操作事务:批量 Create/Updates/Delete 默认在单个事务中执行。如需手动控制,使用
db.Transaction包裹。 - Find 与 First 的性能差异:
Find返回切片,First返回单条。在只需要一条记录时使用First或Take,避免不必要的数据传输。
核心原则:CRUD 操作的选择直接影响数据一致性和 API 行为。Create 关注字段白名单,查询关注未找到处理,更新关注零值策略,删除关注软/硬选择。每个操作都应在事务边界内明确其语义。