This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/GORM/04-条件查询.md
T
2026-05-17 00:06:11 +08:00

306 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [GORM, Go, ORM, 查询, Where, Between, In, Like, Or, Not]
create time: 2026-04-28 00:00
---
# 条件查询
## 概述
条件查询是日常开发中最频繁的数据库操作——从「找出某个用户」到「筛选过去七天活跃的用户」,本质上都是构造 WHERE 子句。GORM 提供了链式的条件 API,让你用 Go 类型安全地构建 SQL 查询,而不是拼接脆弱的字符串。
> [!tip] 核心原则
> GORM 的每个条件方法(`Where` / `Or` / `Not` 等)都返回一个新的 `*gorm.DB` 实例——**不会修改原始 DB**。这意味着你可以反复复用同一个基础查询,像拼乐高一样叠加不同的过滤条件。
## Where 链式调用
### 基本用法
```go
// 单条件
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 注入防护
> **永远不要**把用户输入直接拼进字符串:
> ```go
> // ❌ 危险!用户可注入 `' OR '1'='1`
> db.Where("name = '" + userInput + "'").Find(&users)
>
> // ✅ 正确 —— GORM 会自动参数化
> db.Where("name = ?", userInput).Find(&users)
> ```
### 结构体条件
GORM 支持直接用 struct 作为查询条件——非零值字段会被自动转为 `key = value`:
```go
// 查找姓名为 "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:
> ```go
> db.Where("age > ?", 20).Find(&users)
> // 或者
> db.Where(map[string]any{"age >": 20}).Find(&users) // GORM 会把 ">"" 直接放到 SQL 中
> ```
### 结构化 WHERE 子句
对于复杂的嵌套条件,可以用 `map` + `[]any` 组合构建:
```go
// (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 部分,保证运算优先级正确。但如果你需要更精细的控制,可以手动写完整表达式:
> ```go
> db.Where("(name = ? OR name = ?) AND age > ?", "john", "jane", 18).Find(&users)
> ```
## 常用条件操作符
### In / Not In
```go
// 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。**使用前务必校验切片的长度**:
> ```go
> ids := []int{1, 2, 3}
> if len(ids) == 0 {
> return nil, nil // 没有 ID,直接返回空结果
> }
> db.Where("id IN (?)", ids).Find(&users)
> ```
### Between / Not Between
```go
// 范围查询
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 模糊匹配
```go
// 前缀匹配
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
```go
// 查字段为 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 排除查询
```go
// 排除特定状态的用户
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 条件组合
```go
// 找 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 的条件是「左结合」的。下面两种写法的语义不同:
>
> ```go
> // 写法 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:
```go
// 使用 ? 占位符(安全,防止注入)
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 直接执行
```go
// 执行任意查询并扫描到自定义变量
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`,后续在不同业务场景上追加不同操作:
```go
// 基础条件:只查活跃数据
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 片段。
## 条件查询决策图
```mermaid
flowchart TD
Start["收到查询请求"] --> Type{"条件类型"}
Type --> Exact["精确匹配\nWhere field, value"]
Type --> RangeQ{"范围查询\nBETWEEN 还是 GT/LT"}
RangeQ --> Between["GORM 自带\nBETWEEN ? AND ?"]
RangeQ --> LtGt["自定义 < >\nfield > ? AND field < ?"]
Type --> SetQ{"集合\nIN 还是 NOT IN"}
SetQ --> InOp["IN\nIn field VALUES"]
SetQ --> NotInOp["NOT IN\nNot In field VALUES"]
Type --> LikeQ{"模糊匹配\n前缀 / 后缀 / 全匹配"}
LikeQ --> PrefixLike["LIKE 'prefix%'"]
LikeQ --> SuffixLike["LIKE '%suffix'"]
LikeQ --> FullLike["LIKE '%keyword%'"]
Type --> NullQ{"NULL 检查\nIS NULL 还是 NOT"}
NullQ --> IsNullOp["IS NULL"]
NullQ --> IsNotNullOp["IS NOT NULL"]
Type --> AndQ{"复合条件\n简单 AND 还是 OR 混合"}
AndQ --> SimpleAnd["简单 AND\n连续 Where 调用"]
AndQ --> OrChain["OR 混合\nWhere + Or 链式"]
Type --> Raw["特殊 SQL\nRaw + Expr"]
style Start fill:#4FC08D,color:#fff
style Exact fill:#3B82F6,color:#fff
style Between fill:#3B82F6,color:#fff
style LtGt fill:#3B82F6,color:#fff
style InOp fill:#3B82F6,color:#fff
style NotInOp fill:#3B82F6,color:#fff
style LikeQ fill:#F59E0B,color:#000
style PrefixLike fill:#F59E0B,color:#000
style SuffixLike fill:#F59E0B,color:#000
style FullLike fill:#F59E0B,color:#000
style NullQ fill:#8B5CF6,color:#fff
style IsNullOp fill:#8B5CF6,color:#fff
style IsNotNullOp fill:#8B5CF6,color:#fff
style AndQ fill:#EC4899,color:#fff
style SimpleAnd fill:#EC4899,color:#fff
style OrChain fill:#EC4899,color:#fff
style Raw fill:#EC4899,color:#fff
style Start fill:#4FC08D,color:#fff
```
## 常见坑点速查
| 问题 | 原因 | 解决方案 |
|------|------|---------|
| 空切片传入 `Where("id IN (?)", ids)` | 生成 `IN ()` 非法 SQL | 先用 `len(ids) > 0` 判断 |
| LIKE 查询太慢 | 索引失效导致全表扫描 | 大数据量改用全文检索引擎 |
| 更新零值字段没生效 | `Updates(struct)` 跳过零值 | 改用 `Updates(map)` |
| Or 条件没有预期效果 | 运算符优先级问题 | 用 `()` 显式分组 |
| Rows 没有 Close | 连接泄漏 | `defer rows.Close()` |
## 关联笔记
- [[01-安装与初始化]]
- [[02-模型定义]]
- [[03-CRUD 操作]]
- [[05-关联查询]]
- [[06-排序与分页]]