4. 查询进阶:Where条件、Not/Or条件、Order/Group/Having、Limit/Offset分页
在GORM中,基础查询(First、Find、Take)只能满足最简单的数据获取需求。实际Web服务中,超过80%的查询需要组合条件、排序、分组聚合或分页。本章深入GORM的查询构建器(Query Builder),掌握从简单过滤到复杂分析的全套链式操作。
4.1 Where条件:结构化与动态过滤
GORM的Where方法支持三种传参模式,分别对应不同的使用场景:
| 模式 | 示例 | 适用场景 |
|---|---|---|
| 等值条件(Map) | Where(map[string]interface{}{"status": "active", "age": 30}) |
动态构建多个AND等值条件,零值字段自动忽略 |
| 结构化条件(Struct) | Where(&User{Status: "active", Age: 30}) |
从已有结构体实例提取非零值字段作为条件 |
| 原生SQL(字符串) | Where("age > ? AND status = ?", 25, "active") |
复杂比较、LIKE、IN、BETWEEN等非等值操作 |
关键区别:Map模式会包含所有字段(包括零值),而Struct模式会自动忽略零值字段(0、""、false、nil)。在构建REST API查询参数时,推荐使用Map模式配合
omitempty标签做动态过滤。
// 原生SQL模式:复杂条件
var users []User
db.Where("age BETWEEN ? AND ?", 18, 60).
Where("name LIKE ?", "%张%").
Find(&users)
// Map模式:动态构建(推荐用于API查询)
conditions := map[string]interface{}{}
if name != "" {
conditions["name"] = name
}
if minAge > 0 {
conditions["age >= ?"] = minAge // 注意:Map也支持操作符
}
db.Where(conditions).Find(&users)
// Struct模式:从已有对象提取条件
searchUser := User{Status: "active", Age: 0} // Age=0会被忽略
db.Where(&searchUser).Find(&users)
// 生成的SQL:SELECT * FROM users WHERE status = 'active'
4.2 Not/Or条件:逻辑组合与排除
单一Where链默认使用AND连接。当需要OR逻辑或NOT排除时,使用Or和Not方法。它们支持与Where完全相同的三种传参模式。
// OR条件:查询status为active或age大于30的用户
db.Where("status = ?", "active").
Or("age > ?", 30).
Find(&users)
// SQL: SELECT * FROM users WHERE status = 'active' OR age > 30
// NOT条件:排除特定状态
db.Not("status", "deleted").Find(&users)
// SQL: SELECT * FROM users WHERE status NOT IN ('deleted')
// 混合AND/OR:使用分组子查询
db.Where("role = ?", "admin").
Where(db.Where("age > ?", 30).Or("experience > ?", 5)).
Find(&users)
// SQL: SELECT * FROM users WHERE role = 'admin' AND (age > 30 OR experience > 5)
常见陷阱:在链式调用中,
Or的优先级容易出错。如果需要将OR条件包裹在AND内部,必须使用db.Where(subQuery)的方式传入一个*gorm.DB实例作为子查询,否则OR会与前面的所有AND条件平级。
4.3 Order/Group/Having:排序与聚合
数据排序和分组聚合是报表类接口的核心能力。GORM通过Order、Group、Having方法直接映射SQL语义。
// 排序:多字段排序
db.Order("age DESC, created_at ASC").Find(&users)
// 分组聚合:按状态统计用户数
type Result struct {
Status string
Count int
}
var results []Result
db.Model(&User{}).
Select("status, count(*) as count").
Group("status").
Having("count(*) > ?", 10).
Find(&results)
// 复杂聚合:按月份统计订单金额
db.Model(&Order{}).
Select("DATE_FORMAT(created_at, '%Y-%m') as month, SUM(amount) as total").
Group("month").
Having("total > ?", 1000).
Order("month DESC").
Find(&results)
下表总结了排序与聚合的常用方法及SQL对应关系:
| GORM方法 | SQL对应 | 说明 |
|---|---|---|
Order("col DESC") |
ORDER BY col DESC |
支持多字段,逗号分隔 |
Order("col ASC") |
ORDER BY col ASC |
默认升序,可省略ASC |
Group("col") |
GROUP BY col |
常与Select中的聚合函数配合 |
Having("condition") |
HAVING condition |
对分组后的结果过滤,不能使用WHERE替代 |
4.4 Limit/Offset分页:高效数据切片
分页是Web API的标配功能。GORM通过Limit和Offset实现物理分页,配合Count获取总记录数,即可构建完整的分页响应。
// 基础分页:每页20条,跳过前40条(第3页)
db.Limit(20).Offset(40).Find(&users)
// 完整分页模式:返回数据+总数
var total int64
db.Model(&User{}).Count(&total) // 先统计总数
page := 3
pageSize := 20
offset := (page - 1) * pageSize
db.Limit(pageSize).Offset(offset).Find(&users)
// 返回结构
type PageResult struct {
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
性能建议:对于大表(百万级以上),
OFFSET越深性能越差。此时推荐使用游标分页(Keyset Pagination):WHERE id > last_id ORDER BY id LIMIT 20。GORM原生支持这种方式,只需在Where中传入上一页最后一条记录的ID即可。
4.5 完整查询示例:组合所有条件
以下代码展示了一个真实API接口中,如何将上述所有查询进阶方法组合使用:
func ListOrders(db *gorm.DB, req *OrderListRequest) (*PageResult, error) {
query := db.Model(&Order{})
// 1. 动态条件
if req.Status != "" {
query = query.Where("status = ?", req.Status)
}
if req.MinAmount > 0 {
query = query.Where("amount >= ?", req.MinAmount)
}
if req.StartDate != nil {
query = query.Where("created_at >= ?", req.StartDate)
}
// 2. 排除已取消订单
query = query.Not("status", "cancelled")
// 3. 统计总数(在分页前执行)
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, err
}
// 4. 排序
if req.SortBy != "" {
query = query.Order(req.SortBy + " " + req.SortDir) // 注意防SQL注入
} else {
query = query.Order("created_at DESC")
}
// 5. 分页
offset := (req.Page - 1) * req.PageSize
query = query.Limit(req.PageSize).Offset(offset)
// 6. 执行查询
var orders []Order
if err := query.Find(&orders).Error; err != nil {
return nil, err
}
return &PageResult{
Data: orders,
Total: total,
Page: req.Page,
PageSize: req.PageSize,
}, nil
}
4.6 常见陷阱与最佳实践
- Count必须在分页前执行:一旦调用了
Limit或Offset,Count的结果会被截断。正确做法是先Count,再链式调用Limit/Offset。 - Order防注入:如果排序字段来自用户输入,不要直接拼接字符串。建议使用白名单校验:
allowedSorts := map[string]bool{"created_at": true, "amount": true}。 - 零值陷阱:Struct模式中,
Age: 0会被忽略。如果需要查询年龄为0的记录,必须使用Map模式或原生SQL。 - 子查询性能:在
Where中嵌套*gorm.DB时,GORM会生成子查询。注意子查询的索引覆盖,避免全表扫描。