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替代
GORM查询构建器链式调用流程 DB.Model(&User{}) Where / Not / Or Order Group / Having Limit / Offset Find / Scan 说明: 链式调用顺序不影响最终SQL语义,GORM内部会按SQL逻辑重新排列。但建议按此顺序书写以保持可读性。

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
}
分页查询执行顺序与SQL对应 1. Count(&total) 2. Where / Not / Or 3. Order 4. Limit / Offset 5. Find(&orders) 生成的SQL顺序: SELECT * FROM orders WHERE status = 'active' AND amount >= 100 ORDER BY created_at DESC LIMIT 20 OFFSET 40

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会生成子查询。注意子查询的索引覆盖,避免全表扫描。