字
字节笔记本
2026年7月20日
GORM 查询进阶:从基础操作到关联关系与性能优化
API中转
¥120
GORM 查询进阶:从基础操作到关联关系与性能优化
GORM 是 Go 语言中最流行的 ORM 框架,日常开发中大部分数据库操作都离不开它。本文梳理 GORM 查询的核心知识点,从基础 CRUD 到关联关系处理,再到 Preload 与 Joins 的选型。
基础查询方法
单条记录查询
go
// First:按主键升序,获取第一条
var user User
db.First(&user)
// SELECT * FROM users ORDER BY id LIMIT 1
// Take:不保证顺序,获取一条
db.Take(&user)
// SELECT * FROM users LIMIT 1
// Last:按主键降序,获取最后一条
db.Last(&user)
// SELECT * FROM users ORDER BY id DESC LIMIT 1三者的区别仅在于排序方式,如果表里没有数据,都会返回 gorm.ErrRecordNotFound。
条件查询的三种写法
go
// 字符串条件
db.Where("name = ?", "jinzhu").First(&user)
// 结构体条件(零值字段会被忽略)
db.Where(&User{Name: "jinzhu", Age: 20}).Find(&users)
// SELECT * FROM users WHERE name = 'jinzhu' AND age = 20
// Map 条件
db.Where(map[string]interface{}{"name": "jinzhu", "age": 20}).Find(&users)结构体条件适合简单查询,但要注意零值陷阱:Age: 0 会被忽略。需要查零值时用 Map 或字符串条件。
常用条件类型
go
// IN
db.Where("age IN ?", []int{20, 21, 22}).Find(&users)
// BETWEEN
db.Where("created_at BETWEEN ? AND ?", start, end).Find(&users)
// LIKE
db.Where("name LIKE ?", "%jin%").Find(&users)
// NOT
db.Not("name = ?", "jinzhu").Find(&users)
// OR
db.Where("role = ?", "admin").Or("role = ?", "super_admin").Find(&users)分页与聚合
通用分页函数
go
func GetUsersByPage(db *gorm.DB, page, pageSize int) ([]User, int64, error) {
var users []User
var total int64
db.Model(&User{}).Count(&total)
offset := (page - 1) * pageSize
result := db.Offset(offset).Limit(pageSize).Find(&users)
return users, total, result.Error
}聚合查询
go
// 单列提取
var names []string
db.Model(&User{}).Pluck("name", &names)
// Count
var count int64
db.Model(&User{}).Where("status = ?", "active").Count(&count)
// Select + 聚合函数
type Result struct {
Status string
Total int64
}
var results []Result
db.Model(&User{}).
Select("status, count(*) as total").
Group("status").
Scan(&results)FirstOrInit / FirstOrCreate
go
// 找不到则初始化(不写入数据库)
var user User
db.Where(User{Name: "jinzhu"}).FirstOrInit(&user)
// 找不到则创建(写入数据库)
db.Where(User{Name: "jinzhu"}).FirstOrCreate(&user)Scopes:可复用的查询条件
go
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active")
}
func Recent(db *gorm.DB) *gorm.DB {
return db.Where("created_at > ?", time.Now().AddDate(0, -1, 0))
}
// 组合使用
var users []User
db.Scopes(ActiveUsers, Recent).Find(&users)Scopes 的好处是查询逻辑可以定义一次、到处组合,避免重复代码。
关联关系
GORM 支持四种关联关系,通过结构体字段和 tag 声明。
Has One(一对一)
go
type User struct {
ID uint
Profile Profile
}
type Profile struct {
ID uint
UserID uint // 外键在子表
Bio string
}Has Many(一对多)
go
type User struct {
ID uint
Orders []Order
}
type Order struct {
ID uint
UserID uint // 外键在子表
Amount float64
}Many2Many(多对多)
go
type User struct {
ID uint
Languages []Language `gorm:"many2many:user_languages;"`
}
type Language struct {
ID uint
Name string
}
// 自动创建中间表 user_languagesBelongs To(属于)
go
type Order struct {
ID uint
UserID uint // 外键在己方
User User
}Preload:预加载关联数据
go
// 基础预加载
db.Preload("Orders").Find(&users)
// 条件预加载
db.Preload("Orders", "amount > ?", 100).Find(&users)
// 嵌套预加载
db.Preload("Orders.Items").Find(&users)
// 函数式条件预加载
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at DESC").Limit(5)
}).Find(&users)关联操作 API
go
// 添加关联
db.Model(&user).Association("Languages").Append([]Language{lang1, lang2})
// 删除关联
db.Model(&user).Association("Languages").Delete(&lang1)
// 替换关联
db.Model(&user).Association("Languages").Replace([]Language{lang3})
// 清空关联
db.Model(&user).Association("Languages").Clear()
// 统计数量
count := db.Model(&user).Association("Languages").Count()Preload vs Joins:如何选择
这是 GORM 使用中最常见的疑问之一。
Preload:两条 SQL,嵌套结构
go
var users []User
db.Preload("Orders").Find(&users)
// SQL 1: SELECT * FROM users
// SQL 2: SELECT * FROM orders WHERE user_id IN (1,2,3)结果:每个 user 的 Orders 字段被自动填充,得到嵌套结构。
Joins:一条 SQL,扁平结构
go
var users []User
db.Joins("Profile").Find(&users)
// SELECT users.*, Profile.id AS Profile__id, Profile.bio AS Profile__bio
// FROM users LEFT JOIN profiles ON profiles.user_id = users.id结果:Profile 的字段被合并到 User 结构体中(通过嵌套结构体)。
核心区别
| 维度 | Preload | Joins |
|---|---|---|
| SQL 数量 | 2 条 | 1 条 |
| 结果形态 | 嵌套结构 | 扁平结构 |
| 适用场景 | 获取完整关联数据 | 用关联表条件筛选主表 |
| WHERE 过滤 | 只能过滤关联本身 | 可以过滤主表 |
| 聚合统计 | 不支持 | 支持 |
实际场景选择
go
// 场景 1:获取用户及其所有订单 → Preload
db.Preload("Orders").Find(&users)
// 场景 2:查"有未完成订单的用户" → Joins
db.Joins("JOIN orders ON orders.user_id = users.id").
Where("orders.status = ?", "pending").
Find(&users)
// 场景 3:查用户的订单总金额 → Joins + 聚合
db.Joins("JOIN orders ON orders.user_id = users.id").
Select("users.*, SUM(orders.amount) as total_amount").
Group("users.id").
Find(&users)
// 场景 4:先筛选再补全 → 混合使用
db.Joins("JOIN orders ON orders.user_id = users.id").
Where("orders.status = ?", "pending").
Preload("Orders").
Find(&users)生产环境:无外键设计
生产环境通常不使用数据库层面的外键约束,改用应用层保证数据一致性。
模型定义
go
type User struct {
ID uint `gorm:"primaryKey"`
Name string `gorm:"index"`
}
type Profile struct {
ID uint `gorm:"primaryKey"`
UserID uint `gorm:"index"` // 普通索引,不是外键
Bio string
}事务保证一致性
go
func CreateUserWithProfile(db *gorm.DB, user *User, profile *Profile) error {
return db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(user).Error; err != nil {
return err
}
profile.UserID = user.ID
return tx.Create(profile).Error
})
}批量操作
go
// 分批插入,每次 100 条
db.CreateInBatches(&users, 100)
// 批量更新
db.Model(&User{}).
Where("id IN ?", userIDs).
Update("status", "active")调试技巧
go
// 打印实际执行的 SQL
db.Debug().Where("name = ?", "jinzhu").First(&user)
// 检查是否命中 N+1
// 不好的写法(N+1)
var users []User
db.Find(&users)
for _, u := range users {
db.Where("user_id = ?", u.ID).Find(&u.Orders) // 每个用户一次查询
}
// 正确写法(1 次或 2 次查询)
db.Preload("Orders").Find(&users)总结
GORM 查询的核心在于理解它的链式 API 设计和关联关系处理方式。基础查询用 Where + Find/First,复杂条件用 Or/Not/IN 组合,分页用 Offset + Limit。关联关系方面,Preload 适合获取完整嵌套数据,Joins 适合用关联表做筛选和聚合。生产环境建议去掉物理外键,用索引 + 事务替代。
分享: