306 lines
9.8 KiB
Markdown
306 lines
9.8 KiB
Markdown
---
|
||
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-排序与分页]]
|