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-04-28 20:23:33 +08:00

287 lines
9.1 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
db.Not("id IN (?)", []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)`。
### Condition 对象(条件复用)
```go
cond := gorm.Condition.Where("status = ?", "active").Or("status = ?", "pending")
db.Where(cond).Find(&users)
// 多个条件组合复用
activeCount := db.Model(&User{}).Where(cond).Count()
activeUsers := db.Where(cond).Order("created_at DESC").Limit(10).Find(&users)
```
> [!tip] 何时使用 Condition 对象?
> 当你需要在多个查询中重复使用同一组条件时(比如项目中所有业务模块都要「只查活跃数据」),把它抽成全局 Condition 变量,避免到处复制粘贴 SQL 片段。
## 条件查询决策图
```mermaid
flowchart TD
Start[收到查询请求] --> Type{条件类型_}
Type --> |精确匹配| Exact["Where(field, value)"]
Type --> |范围查询| RangeQ{BETWEEN 还是 _}
RangeQ --> |GORM 自带| Between["BETWEEN ? AND ?"]
RangeQ --> |自定义> < |=LtGt["where 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 还是 _}
NullQ --> |IS NULL|IsNull["IS NULL"]
NullQ --> |IS NOT NULL| IsNotNull["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()` |
## 关联笔记
- [[01-安装与初始化]]
- [[02-模型定义]]
- [[03-CRUD 操作]]
- [[05-关联查询]]
- [[06-排序与分页]]