查询进阶
本章覆盖 GORM 的高级查询能力:条件构建器全解、排序分页、分组聚合、JOIN、子查询、Scan 到自定义结构。
Where 条件大全
基础条件
// 占位符参数(推荐:防 SQL 注入)
db.Where("name = ?", "张三")
db.Where("age > ? AND status = ?", 18, "active")
// 参数展开(变长 IN 查询)
db.Where("id IN ?", []int{1, 2, 3})
// SELECT * FROM users WHERE id IN (1, 2, 3)
// 命名参数(复杂查询时更清晰)
db.Where("name = @name AND age > @age", sql.Named("name", "张三"), sql.Named("age", 18))
// SELECT * FROM users WHERE name = '张三' AND age > 18Caution
🚨 陷阱:永远不要用 fmt.Sprintf 拼接用户输入到 SQL 中!始终用占位符 ? 传参。
结构体 / Map 条件
// 结构体:只匹配非零字段
db.Where(&User{Name: "张三", Age: 18})
// SELECT * FROM users WHERE name = '张三' AND age = 18
db.Where(&User{Name: "张三", Age: 0})
// SELECT * FROM users WHERE name = '张三' ← Age=0 被忽略
// Map:所有字段参与匹配
db.Where(map[string]interface{}{"name": "张三", "age": 0})
// SELECT * FROM users WHERE name = '张三' AND age = 0
// 结构体 + Map 组合
db.Where(&User{Name: "张三"}).Where(map[string]interface{}{"age": 18})运算符速查
db.Where("name = ?", "张三") // =
db.Where("name <> ?", "张三") // !=
db.Where("name LIKE ?", "%张%") // LIKE
db.Where("name IN ?", []string{"张三", "李四"}) // IN
db.Where("age BETWEEN ? AND ?", 18, 30) // BETWEEN
db.Where("deleted_at IS NULL") // IS NULL
db.Where("deleted_at IS NOT NULL") // IS NOT NULL字符串条件
// GORM 会自动转义(安全)
db.Where("name = ?", "张三")
// 原始字符串(少量场景——不安全,需自行确保安全)
db.Where("name = '张三'")
// 使用 DB.Raw 执行原始 SQL
db.Raw("SELECT * FROM users WHERE name = ?", "张三").Scan(&users)Not / Or 条件
// Not
db.Not("name = ?", "张三").Find(&users)
db.Not(User{Name: "张三"}).Find(&users)
db.Not([]int{1, 2, 3}).Find(&users) // WHERE id NOT IN (1, 2, 3)
// Or
db.Where("age > ?", 18).Or("status = ?", "vip").Find(&users)
db.Where("name = ?", "张三").Or(&User{Age: 18}).Find(&users)
// 括号条件(AND 优先级 > OR)
db.Where("(name = ? AND age > ?) OR status = ?", "张三", 18, "vip").Find(&users)排序
// 单字段排序
db.Order("age DESC").Find(&users)
// 多字段排序
db.Order("age DESC, name ASC").Find(&users)
db.Order("age DESC").Order("name ASC").Find(&users) // 等效
// 动态排序(需要防护)
sortField := "age" // 白名单校验
sortOrder := "desc"
db.Order(clause.OrderByColumn{Column: clause.Column{Name: sortField}, Desc: sortOrder == "desc"}).Find(&users)🚨 陷阱:当排序字段来自用户输入时,不能直接用
fmt.Sprintf拼接到Order中(SQL 注入风险)。应维护白名单映射。
Limit / Offset — 分页
// 基础分页
db.Limit(10).Offset(20).Find(&users)
// SELECT * FROM users LIMIT 10 OFFSET 20
// 负 Limit 取消 LIMIT
db.Limit(10).Find(&users).Limit(-1).Find(&users2) // 第二个查询无 Limit
// 推荐的分页函数
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
if pageSize > 100 {
pageSize = 100 // 限制最大页大小
}
offset := (page - 1) * pageSize
return db.Offset(offset).Limit(pageSize)
}
}
// 使用
db.Scopes(Paginate(2, 10)).Find(&users)深度分页优化
| 方案 | 适用场景 | 实现 |
|---|---|---|
LIMIT OFFSET |
小数据量,前几页 | db.Limit(n).Offset(m) |
| 基于索引(游标分页) | 大数据量,无限滚动 | db.Where("id > ?", lastID).Limit(n) |
| 子查询优化 | 大 OFFSET 时 | db.Where("id IN (SELECT id FROM ... LIMIT ... OFFSET ...)") |
// 游标分页(高效 — 利用主键索引)
var users []User
db.Where("id > ?", lastID).Order("id").Limit(10).Find(&users)
// 记下最后一个 id,作为下次查询的游标Tip
⚡ 性能提示:LIMIT 10000, 20 在 MySQL 中会扫描 10020 行再丢弃前 10000 行。游标分页(基于索引的 WHERE id > lastID)无论偏移多大都只扫描 20 行。
Group By / Having
// 分组统计
type Result struct {
Name string
Count int
Total int
}
var results []Result
db.Model(&User{}).
Select("name, COUNT(*) as count, SUM(age) as total").
Group("name").
Having("count > ?", 1).
Find(&results)
// 多字段分组
db.Model(&Order{}).
Select("user_id, status, COUNT(*) as count").
Group("user_id, status").
Find(&results)Distinct
var names []string
db.Model(&User{}).Distinct("name").Pluck("name", &names)
// SELECT DISTINCT name FROM users
// 搭配其他条件
db.Model(&User{}).Distinct("name", "age").Find(&users)Joins — 连接查询
Inner / Left / Right Join
// Inner Join
db.Joins("Company").Find(&users)
// SELECT users.*, Company.* FROM users INNER JOIN companies ON ...
// Left Join
db.Joins("LEFT JOIN companies ON companies.id = users.company_id").Find(&users)
// 多表 Join
db.Joins("LEFT JOIN companies ON companies.id = users.company_id").
Joins("LEFT JOIN orders ON orders.user_id = users.id").
Find(&users)
// Join + 条件
db.Joins("Company", db.Where(&Company{Status: "active"})).Find(&users)Join + 关联模型
type User struct {
gorm.Model
Name string
CompanyID int
Company Company // 关联
}
// 使用关联名 JOIN(假设 Company 外键已定义)
db.Joins("Company").Where("Company.name = ?", "XX公司").Find(&users)
// Join 到关联表并只查主表字段
db.Joins("Company").Select("users.*").Find(&users)Preload — 预加载关联
Preload 是 GORM 最重要的关联查询方法之一(详情见 05-关联关系):
// 基础 Preload
db.Preload("Orders").Find(&users)
// 条件 Preload
db.Preload("Orders", "status = ?", "paid").Find(&users)
// 嵌套 Preload
db.Preload("Orders.OrderItems").Find(&users)
// 自定义 Preload
db.Preload("Orders", func(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "paid").Order("created_at DESC").Limit(5)
}).Find(&users)SubQuery — 子查询
// WHERE 子查询
db.Where("age > (SELECT AVG(age) FROM users)").Find(&users)
// 用 GORM 构建子查询
subQuery := db.Model(&Order{}).Select("user_id").Where("amount > ?", 100)
db.Where("id IN (?)", subQuery).Find(&users)
// FROM 子查询
db.Table("(?) as u", db.Model(&User{}).Where("age > ?", 18)).Find(&results)
// SELECT 子查询
db.Model(&User{}).Select("*, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) as order_count").Find(&users)Scan — 扫描到自定义结构
// 自定义结果结构
type UserStat struct {
Name string `json:"name"`
Count int `json:"count"`
Total int `json:"total"`
}
// 方式一:Raw SQL
var stats []UserStat
db.Raw("SELECT name, COUNT(*) as count, SUM(age) as total FROM users GROUP BY name").Scan(&stats)
// 方式二:GORM 构建器 + Scan
db.Model(&User{}).
Select("name, COUNT(*) as count, SUM(age) as total").
Group("name").
Scan(&stats)
// 方式三:Find 到自定义结构
db.Model(&User{}).
Select("name, COUNT(*) as count").
Group("name").
Find(&stats)💡
Scan+ 自定义结构体 是 GORM 灵活性最强的查询方式之一。查询结果可以映射到与数据库表无任何关联的任意结构体上。
Scopes — 复用查询条件
// 定义 Scope
func ActiveUsers(db *gorm.DB) *gorm.DB {
return db.Where("status = ?", "active").Where("deleted_at IS NULL")
}
func Adults(db *gorm.DB) *gorm.DB {
return db.Where("age >= ?", 18)
}
func Paginate(page, pageSize int) func(db *gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Offset((page - 1) * pageSize).Limit(pageSize)
}
}
// 使用
db.Scopes(ActiveUsers, Adults, Paginate(1, 20)).Find(&users)💡 最佳实践:将常用的过滤条件封装为 Scope 函数,可使查询代码简洁、可复用、易于测试。
Select — 指定查询字段
// 指定字段
db.Select("name", "age").Find(&users)
// SELECT name, age FROM users
// 表达式
db.Select("name, COALESCE(nickname, name) as display_name").Find(&users)
// 子查询字段
db.Model(&User{}).Select("*, (SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id) as order_count").Find(&users)
// 用 Select 保护敏感字段(不查出密码)
db.Select("id", "name", "email").Find(&users)🚨 陷阱:
db.Select("password").Find(&users)只会选出部分字段,其他字段为零值。除非有意为之,否则用Omit("password")来排除敏感字段更安全。
原生 SQL 查询
// Raw — 查询多行
var users []User
db.Raw("SELECT * FROM users WHERE age > ?", 18).Scan(&users)
// Row — 查询单行
row := db.Raw("SELECT name, age FROM users WHERE id = ?", 1).Row()
var name string
var age int
row.Scan(&name, &age)
// Rows — 遍历多行
rows, _ := db.Raw("SELECT * FROM users WHERE age > ?", 18).Rows()
defer rows.Close()
for rows.Next() {
var user User
db.ScanRows(rows, &user)
}
// Exec — 执行写操作
db.Exec("UPDATE users SET status = ? WHERE last_login < ?", "inactive", time.Now().AddDate(0, -3, 0))常见陷阱汇总
| 陷阱 | 说明 |
|---|---|
| 🚨 SQL 拼接注入 | 绝不将用户输入直接拼到 SQL 字符串中 |
| 🚨 Offset 分页性能 | 大偏移量时 LIMIT big_number, n 极慢 |
| 🚨 Preload N+1 | 循环中使用 Preload 可能产生 N+1 查询 |
| 🚨 Scan 字段匹配 | 自定义结构体字段名需与 SELECT 别名一致(大小写由驱动决定) |
| 🚨 Join 不 Preload | Joins 只做连接查询,不填充关联对象;要填充需同时用 Preload |
| 🚨 空切片 Scan | Scan 到 nil 切片不会报错,也不会填充任何数据 |