Skip to content
Go
基础 CRUD

基础 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.RowsAffectedlen(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 结构体零值不更新,用 mapSelect*bool/*int
🚨 Save 不直观 主键零值 SUPDATE→INSERT,建议用显式 Update/Updates
🚨 全表删除保护 不带 WHERE 的 Delete 会报 ErrMissingWhereClause
🚨 Find 不返回错误 未找到时返回空切片而非错误
🚨 Last 顺序 按主键降序,不是按 CreatedAt
🚨 软删除 + 唯一键 需要部分唯一索引,否则违反唯一约束
🚨 DB 实例传递 确保传递的 *gorm.DB 确实连接到数据库,否则执行时 panic