基础 CRUD
本章覆盖 GORM 中最常用的增删改查操作,包括单条/批量、条件、软删除等。
Create — 创建记录
单条创建
user := User{Name: "张三", Age: 18, Email: "zhangsan@example.com"}
result := db.Create(&user)
fmt.Println(user.ID) // 自动回填主键
fmt.Println(result.RowsAffected) // 1
fmt.Println(result.Error) // nil批量创建
users := []User{
{Name: "张三", Age: 18},
{Name: "李四", Age: 20},
{Name: "王五", Age: 22},
}
// 方式一:单条 INSERT 全部数据
db.Create(&users)
// 方式二:分批插入(大量数据时推荐)
db.CreateInBatches(users, 100) // 每批 100 条
// 方式三:原生 SQL
db.Exec("INSERT INTO users (name, age) VALUES (?, ?), (?, ?)",
"张三", 18, "李四", 20)| 方法 | 适用场景 | 说明 |
|---|---|---|
Create(&slice) |
< 1000 条 | 单条 INSERT,自动分批取决于驱动 |
CreateInBatches(&slice, n) |
> 1000 条 | 显式指定每批数量 |
Create(&record) |
单条 | 回填主键和时间戳 |
🔬 深入原理:MySQL 单条 INSERT 语句有 max_allowed_packet 限制(默认 64MB)。建议每批不超过 1000 条,避免超出。
指定字段创建
// 只创建指定字段
db.Select("Name", "Age").Create(&user)
// 忽略指定字段
db.Omit("Email", "Bio").Create(&user)
// 创建时不触发 Hook
db.Session(&gorm.Session{SkipHooks: true}).Create(&user)从 Map 创建
// map[string]interface{}
db.Model(&User{}).Create(map[string]interface{}{
"Name": "张三",
"Age": 18,
})
// 带类型的 map
db.Model(&User{}).Create(map[string]any{"Name": "张三", "Age": 18})🚨 陷阱:用
map创建时,钩子方法不会被调用,时间戳追踪也不会自动填充,关联也不会保存。
Upsert — 插入或更新
冲突时更新(MySQL)
// MySQL: ON DUPLICATE KEY UPDATE
user := User{ID: 1, Name: "张三改名", Age: 25}
// 方式一:Clauses
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "id"}}, // 冲突列
DoUpdates: clause.AssignmentColumns([]string{"name", "age"}), // 更新列
}).Create(&user)
// 方式二:Clauses + UpdateAll(全部非零值更新)
db.Clauses(clause.OnConflict{
UpdateAll: true,
}).Create(&user)冲突时更新(PostgreSQL)
// PostgreSQL: ON CONFLICT ... DO UPDATE
db.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "email"}}, // 冲突列(需有唯一约束)
DoUpdates: clause.AssignmentColumns([]string{"name"}), // 更新列
}).Create(&user)
// 忽略冲突(重复时不操作)
db.Clauses(clause.OnConflict{DoNothing: true}).Create(&user)FirstOrCreate
// 根据条件查找,未找到则创建
db.Where(User{Email: "test@example.com"}).FirstOrCreate(&user)
// 等价于:SELECT * FROM users WHERE email = 'test@example.com' LIMIT 1
// 未找到 → INSERT INTO users (email) VALUES ('test@example.com')
// 带额外属性的 FirstOrCreate
db.Where(User{Email: "test@example.com"}).
Attrs(User{Name: "默认名", Age: 18}). // 仅在创建时赋值
FirstOrCreate(&user)
// 用 Assign 替代 Attrs(无论创建还是找到,都更新)
db.Where(User{Email: "test@example.com"}).
Assign(User{Name: "强制更新"}).
FirstOrCreate(&user)| 方法 | 行为 |
|---|---|
FirstOrCreate |
找到 → 返回;未找到 → 创建 |
Attrs |
仅在创建时设置的值 |
Assign |
无论创建还是找到都设置的值 |
Read — 查询记录
单条查询
// 按主键
user := User{}
db.First(&user, 1) // SELECT * FROM users WHERE id = 1 ORDER BY id LIMIT 1
db.First(&user, "id = ?", 1) // 同上
// 按条件
db.First(&user, "name = ?", "张三") // 第一个匹配的
db.Take(&user) // 无特定顺序的第一个
db.Last(&user) // 最后一个(按主键降序)
// 未找到记录时,First/Last/Take 返回 gorm.ErrRecordNotFound
err := db.First(&user, 999).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
fmt.Println("用户不存在")
}| 方法 | 行为 | 未找到时 |
|---|---|---|
First |
按主键升序的第一个 | 返回 ErrRecordNotFound |
Last |
按主键降序的第一个 | 返回 ErrRecordNotFound |
Take |
无特定顺序的一个 | 返回 ErrRecordNotFound |
Find |
所有匹配记录 | 返回空切片(无错误) |
🚨 陷阱:
db.Find(&users)未找到时不返回错误,只返回空切片。需要检查result.RowsAffected或len(users)。
按主键列表查询
// 查询指定 ID 的用户
var users []User
db.Find(&users, []int{1, 2, 3})
// SELECT * FROM users WHERE id IN (1, 2, 3)
// 主键为字符串类型
db.Find(&users, []string{"uuid-1", "uuid-2"})查询全部记录
var users []User
result := db.Find(&users)
fmt.Println(result.RowsAffected) // 总行数
// 遍历大量记录(避免一次性加载全部到内存)
rows, _ := db.Model(&User{}).Where("age > ?", 18).Rows()
defer rows.Close()
for rows.Next() {
var user User
db.ScanRows(rows, &user)
// 处理 user
}条件查询
// 获取第一条匹配记录
var user User
db.Where("name = ?", "张三").First(&user)
// 获取所有匹配记录
var users []User
db.Where("age > ?", 18).Find(&users)
// 多条件(AND)
db.Where("name = ? AND age > ?", "张三", 18).Find(&users)
db.Where("name = ?", "张三").Where("age > ?", 18).Find(&users) // 等效完整的查询选择器见 04-查询进阶。
Update — 更新记录
更新单列
db.Model(&User{}).Where("name = ?", "张三").Update("age", 20)
// 更新单列(有条件)
db.Model(&user).Update("name", "张三改名") // user 必须有主键值
// UPDATE users SET name = '张三改名' WHERE id = user.ID批量更新列
// 方式一:结构体(零值不更新)
db.Model(&User{}).Where("age > ?", 18).Updates(User{
Name: "批量改名",
Age: 0, // ← 零值被忽略
IsVIP: false, // ← 零值被忽略
})
// UPDATE users SET name = '批量改名', updated_at = NOW() WHERE age > 18
// 方式二:map(零值也会更新)
db.Model(&User{}).Where("age > ?", 18).Updates(map[string]interface{}{
"name": "批量改名",
"age": 0, // ← 零值会写入
"is_vip": false, // ← 零值会写入
})
// UPDATE users SET name = '批量改名', age = 0, is_vip = false, updated_at = NOW() WHERE age > 18
// 方式三:Select 指定要更新的字段(零值也会写入)
db.Model(&User{}).Where("age > ?", 18).Select("Name", "Age").Updates(User{
Name: "批量改名",
Age: 0, // ← 通过 Select 指定后,零值也会写入
})表达式更新
// 字段自增
db.Model(&User{ID: 1}).Update("age", gorm.Expr("age + ?", 1))
// 多列表达式
db.Model(&User{ID: 1}).Updates(map[string]interface{}{
"age": gorm.Expr("age + ?", 1),
"login_at": gorm.Expr("NOW()"),
})
// 使用子查询
db.Model(&user).Update("follower_count",
db.Model(&Follow{}).Select("COUNT(*)").Where("followed_id = ?", user.ID),
)保存整个结构体
user := User{ID: 1, Name: "张三", Age: 25, Email: "test@example.com"}
db.Save(&user)
// UPDATE users SET name='张三', age=25, email='test@example', updated_at=NOW() WHERE id=1
// Save 基于主键匹配——无主键则 Create
newUser := User{Name: "新用户", Age: 20}
db.Save(&newUser) // INSERT(无主键)| 方法 | 零值处理 | 适用场景 |
|---|---|---|
Save |
零值写入 | 保存整个结构体(包括零值) |
Updates(struct) |
零值不写入 | 部分字段更新 |
Updates(map) |
零值写入 | 需要明确更新零值 |
Update |
单列 | 更新单个列 |
Select+Updates(struct) |
指定字段零值写入 | 精确控制哪些字段写入 |
🚨 陷阱:
Save会保存所有字段,包括零值。如果没有设置条件,它基于主键更新——但若主键是零值,会变成INSERT。这与直觉不符,建议避免使用Save,改用显式的Update/Updates。
Delete — 删除记录
软删除(默认行为)
GORM 使用 DeletedAt 字段实现软删除:
type User struct {
gorm.Model // 内含 DeletedAt gorm.DeletedAt
Name string
}
// 删除 → 设置 DeletedAt = NOW()
db.Delete(&User{}, 1)
// UPDATE users SET deleted_at = NOW() WHERE id = 1
// 此后所有查询自动过滤已删除记录
db.Find(&users) // WHERE deleted_at IS NULL硬删除
// Unscoped 绕过软删除过滤
db.Unscoped().Delete(&User{}, 1)
// DELETE FROM users WHERE id = 1
// 批量硬删除
db.Unscoped().Where("age < ?", 18).Delete(&User{})查询已删除记录
// 只查已删除的记录
var deletedUsers []User
db.Unscoped().Where("deleted_at IS NOT NULL").Find(&deletedUsers)
// 查所有记录(包括已删除)
db.Unscoped().Find(&users)批量删除
// 按条件批量删除(必须带 WHERE,否则报错)
db.Where("age < ?", 18).Delete(&User{})
// 按主键列表
db.Delete(&User{}, []int{1, 2, 3})
// 带 WHERE 的速记
db.Delete(&User{}, "name = ?", "张三")🚨 陷阱:GORM v2 中,
db.Delete(&User{})(无 WHERE)会报gorm.ErrMissingWhereClause错误。这是安全保护,防止误删全表。如果确实要删除全表,使用db.Where("1 = 1").Delete(&User{})或db.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&User{})。
软删除 + 唯一索引注意事项
// 软删除场景下,唯一索引需包含 deleted_at
type User struct {
gorm.Model
Email string `gorm:"uniqueIndex:,where:deleted_at IS NULL"` // 部分唯一索引
}
// 或使用复合唯一索引
type User struct {
gorm.Model
Email string `gorm:"uniqueIndex:idx_email_deleted,expression:DEFAULT"` // MySQL
}💡 最佳实践:软删除 + 唯一字段时,使用部分唯一索引
WHERE deleted_at IS NULL,否则删除后无法用相同的唯一值创建新记录。
Count — 统计行数
var count int64
// 统计全表
db.Model(&User{}).Count(&count)
// 带条件的统计
db.Model(&User{}).Where("age > ?", 18).Count(&count)
// 分组统计
db.Model(&User{}).Select("age, COUNT(*) as count").Group("age").Find(&results)Pluck — 提取单列
var ages []int
db.Model(&User{}).Pluck("age", &ages)
var names []string
db.Model(&User{}).Where("age > ?", 18).Pluck("name", &names)
// 提取多列到 map
var result []map[string]interface{}
db.Model(&User{}).Where("age > ?", 18).Find(&result)常见陷阱汇总
| 陷阱 | 说明 |
|---|---|
| 🚨 零值 Updates | 结构体零值不更新,用 map、Select 或 *bool/*int |
| 🚨 Save 不直观 | 主键零值 SUPDATE→INSERT,建议用显式 Update/Updates |
| 🚨 全表删除保护 | 不带 WHERE 的 Delete 会报 ErrMissingWhereClause |
| 🚨 Find 不返回错误 | 未找到时返回空切片而非错误 |
| 🚨 Last 顺序 | 按主键降序,不是按 CreatedAt |
| 🚨 软删除 + 唯一键 | 需要部分唯一索引,否则违反唯一约束 |
| 🚨 DB 实例传递 | 确保传递的 *gorm.DB 确实连接到数据库,否则执行时 panic |