9.4 KiB
tags, create time
| tags | create time | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
2026-04-28 00:00 |
条件查询
概述
条件查询是日常开发中最频繁的数据库操作——从「找出某个用户」到「筛选过去七天活跃的用户」,本质上都是构造 WHERE 子句。GORM 提供了链式的条件 API,让你用 Go 类型安全地构建 SQL 查询,而不是拼接脆弱的字符串。
[!tip] 核心原则 GORM 的每个条件方法(
Where/Or/Not等)都返回一个新的*gorm.DB实例——不会修改原始 DB。这意味着你可以反复复用同一个基础查询,像拼乐高一样叠加不同的过滤条件。
Where 链式调用
基本用法
// 单条件
db.Where("name = ?", "john").Find(&users)
// SELECT * FROM users WHERE name = 'john';
// 多条件(AND 关系)
db.Where("name = ? AND age >= ?", "john", 20).Find(&users)
// SELECT * FROM users WHERE name = 'john' AND age >= 20;
[!warning] SQL 注入防护 永远不要把用户输入直接拼进字符串:
// ❌ 危险!用户可注入 `' OR '1'='1` db.Where("name = '" + userInput + "'").Find(&users) // ✅ 正确 —— GORM 会自动参数化 db.Where("name = ?", userInput).Find(&users)
结构体条件
GORM 支持直接用 struct 作为查询条件——非零值字段会被自动转为 key = value:
// 查找姓名为 "john" 且年龄大于等于 20 的用户
db.Where(&User{Name: "john", Age: 20}).Find(&users)
// SELECT * FROM users WHERE name = 'john' AND age = 20;
// map 方式(键为字段名或列名)
db.Where(map[string]any{"name": "john", "age": 20}).Find(&users)
// SELECT * FROM users WHERE name = 'john' AND age = 20;
[!question] 为什么结构体条件用的是
=而非>=? 因为20就是字面值,不存在比较运算符的问题。如果要做范围查询,需要用 Map 方式配合自定义 key:db.Where("age > ?", 20).Find(&users) // 或者 db.Where(map[string]any{"age >": 20}).Find(&users) // GORM 会把 ">"" 直接放到 SQL 中
结构化 WHERE 子句
对于复杂的嵌套条件,可以用 map + []any 组合构建:
// (name = john AND age >= 20) OR (role = admin)
db.Where(
"name = ? AND age >= ?", "john", 20,
).Or(
"role = ?", "admin",
).Find(&users)
// SELECT * FROM users WHERE (name = 'john' AND age >= 20) OR (role = 'admin');
[!tip] 括号处理 GORM 会自动为
Where+Or组合加括号包裹 WHERE 部分,保证运算优先级正确。但如果你需要更精细的控制,可以手动写完整表达式:db.Where("(name = ? OR name = ?) AND age > ?", "john", "jane", 18).Find(&users)
常用条件操作符
In / Not In
// IN 查询
db.Where("id IN (?)", []int{1, 2, 3}).Find(&users)
// SELECT * FROM users WHERE id IN (1, 2, 3);
// 批量删除
db.Where("id IN (?)", []int{1, 2, 3}).Delete(&User{})
// NOT IN —— GORM Not 方法接收字段名和切片,自动推断 NOT IN
db.Not("id", []int{1, 2, 3}).Find(&users)
// SELECT * FROM users WHERE id NOT IN (1, 2, 3);
[!tip] 空切片陷阱
[]int{}(空切片)传进IN (?)时,GORM 会生成IN ()—— 这是非法 SQL。使用前务必校验切片的长度:ids := []int{1, 2, 3} if len(ids) == 0 { return nil, nil // 没有 ID,直接返回空结果 } db.Where("id IN (?)", ids).Find(&users)
Between / Not Between
// 范围查询
db.Where("age BETWEEN ? AND ?", 18, 60).Find(&users)
// SELECT * FROM users WHERE age BETWEEN 18 AND 60;
// Go 风格写法(等价)
db.Where("age >= ? AND age <= ?", 18, 60).Find(&users)
// 时间范围(按天查最近 7 天的订单)
startTime := time.Now().AddDate(0, 0, -7)
db.Where("created_at BETWEEN ? AND ?", startTime, time.Now()).Find(&orders)
Like 模糊匹配
// 前缀匹配
db.Where("name LIKE ?", "john%").Find(&users) // 以 john 开头
// SELECT * FROM users WHERE name LIKE 'john%';
// 后缀匹配
db.Where("email LIKE ?", "%@gmail.com").Find(&users) // gmail 邮箱
// 包含匹配
db.Where("title LIKE ?", "%go编程%").Find(&articles) // 标题包含关键词
[!warning] LIKE 的性能问题
%xxx%这种前后通配会导致索引失效——数据库无法使用 B+ 树的有序性,退化成全表扫描。当数据量超过 10 万行时,考虑改用专门的搜索引擎(如 Elasticsearch)。
Is NULL / Is NOT NULL
// 查字段为 NULL 的记录
db.Where("deleted_at IS NULL").Find(&users)
// 注意:这其实是 GORM 软删除的默认行为,通常不用显式写
// 查字段不为 NULL
db.Where("phone IS NOT NULL").Find(&users)
// GORM 简洁写法
db.Where("phone ?", "not null").Find(&users)
Not 排除查询
// 排除特定状态的用户
db.Not("status", "banned").Find(&users)
// SELECT * FROM users WHERE status != 'banned';
// 排除多个值
db.Not([]string{"banned", "inactive"}).Find(&users)
// SELECT * FROM users WHERE status NOT IN ('banned', 'inactive');
// 多字段排除
db.Not(map[string]any{"name": "john", "role": "admin"}).Find(&users)
// SELECT * FROM users WHERE name != 'john' AND role != 'admin';
Or 条件组合
// 找 VIP 用户或注册时间超过一年的用户
db.Where("role = ?", "vip").Or("created_at < ?", time.Now().AddDate(-1, 0, 0)).Find(&users)
// SELECT * FROM users WHERE (role = 'vip') OR (created_at < '2025-04-28...');
[!example] Where 和 Or 的执行顺序 GORM 的条件是「左结合」的。下面两种写法的语义不同:
// 写法 A: (a AND b) OR c db.Where("a = 1").Where("b = 2").Or("c = 3") // 写法 B: a AND (b OR c) db.Where("a = 1").Or("b = 2 AND c = 3")建议在复杂场景下用
()显式控制优先级,让意图一目了然。
高级条件构造
原始 SQL
当 GORM 提供的 DSL 不够用时,可以直接写原生 SQL:
// 使用 ? 占位符(安全,防止注入)
db.Where("UNIX_TIMESTAMP(created_at) > ?", time.Now().AddDate(0,0,-1).Unix()).Find(&users)
// 使用 gorm.Expr 处理更复杂的表达式
db.Where("price * ? > ?", 0.8, 100).Find(&products) // price * 0.8 > 100
Row / Scan 直接执行
// 执行任意查询并扫描到自定义变量
var count int
db.Raw("SELECT COUNT(*) FROM users WHERE status = ?", "active").Scan(&count)
// Query 返回 *sql.Row / *sql.Rows,适合更底层的操作
rows, err := db.Raw("SELECT id, name FROM users").Rows()
defer rows.Close()
for rows.Next() {
var id int
var name string
rows.Scan(&id, &name)
// ...
}
[!important] 资源管理
Rows对象持有数据库连接,使用完毕后必须调用rows.Close(),否则会导致连接泄漏。如果只需要单行结果,优先用Raw(...).Scan(&dest)。
条件复用(中间态 DB)
把带条件的查询保存为一个 *gorm.DB,后续在不同业务场景上追加不同操作:
// 基础条件:只查活跃数据
q := db.Model(&User{}).Where("status = ?", "active")
// 统计活跃用户数
activeCount := q.Count()
// 分页查询最新活跃用户
activeUsers := make([]User, 0)
q.Order("created_at DESC").Limit(10).Find(&activeUsers)
[!tip] 何时使用中间态 DB? 当你需要在多个查询中重复使用同一组条件时(比如项目中所有业务模块都要「只查活跃数据」),先构建好基础查询再分别追加
Order/Limit/Select等操作,避免到处复制粘贴 SQL 片段。
条件查询决策图
flowchart TD
Start["收到查询请求"] --> Type{"条件类型"}
Type --> "精确匹配" --> Exact["Where field, value"]
Type --> "范围查询" --> RangeQ{"BETWEEN 还是 GT/LT"}
RangeQ --> "GORM 自带" --> Between["BETWEEN ? AND ?"]
RangeQ --> "自定义 < >" --> LtGt["field > ? AND field < ?"]
Type --> "集合" --> SetQ{"IN 还是 NOT IN"}
SetQ --> "IN" --> InOp["In field VALUES"]
SetQ --> "NOT IN" --> NotInOp["Not In field VALUES"]
Type --> "模糊" --> LikeQ{"前缀/后缀/全匹配"}
LikeQ --> "前缀 %" --> PrefixLike["LIKE 'prefix%'"]
LikeQ --> "后缀 %" --> SuffixLike["LIKE '%suffix'"]
LikeQ --> "全匹配 %" --> FullLike["LIKE '%keyword%'"]
Type --> "NULL检查" --> NullQ{"IS NULL 还是 NOT"}
NullQ --> "IS NULL" --> IsNullOp["IS NULL"]
NullQ --> "IS NOT NULL" --> IsNotNullOp["IS NOT NULL"]
Type --> "复合条件" --> AndQ{"简单AND 还是 OR混合"}
AndQ --> "简单AND" --> SimpleAnd["连续 Where 调用"]
AndQ --> "OR混合" --> OrChain["Where + Or 链式"]
Type --> "特殊SQL" --> Raw["Raw + Expr"]
style Start fill:#4FC08D,color:#fff
style Exact fill:#3B82F6,color:#fff
style LikeOp fill:#F59E0B,color:#000
style Raw fill:#EC4899,color:#fff
常见坑点速查
| 问题 | 原因 | 解决方案 |
|---|---|---|
空切片传入 Where("id IN (?)", ids) |
生成 IN () 非法 SQL |
先用 len(ids) > 0 判断 |
| LIKE 查询太慢 | 索引失效导致全表扫描 | 大数据量改用全文检索引擎 |
| 更新零值字段没生效 | Updates(struct) 跳过零值 |
改用 Updates(map) |
| Or 条件没有预期效果 | 运算符优先级问题 | 用 () 显式分组 |
| Rows 没有 Close | 连接泄漏 | defer rows.Close() |