Skip to content
Go
查询进阶

查询进阶

本章覆盖 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 > 18

Caution

🚨 陷阱:永远不要用 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 切片不会报错,也不会填充任何数据