--- 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-排序与分页]]