Files
cs-note/hhs/GORM/04-条件查询.md
T
2026-05-24 11:42:38 +08:00

9.8 KiB
Raw Blame History

tags, create time
tags create time
GORM
Go
ORM
查询
Where
Between
In
Like
Or
Not
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["精确匹配\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()

关联笔记