动态SQL ============ .. note:: 动态SQL是一种强大的数据库查询技术,它允许在运行时根据不同条件动态生成SQL语句。这种技术特别适用于复杂的查询场景,如条件搜索、批量操作和灵活的数据操作。动态SQL不仅提高了代码的复用性和可维护性,还能优化查询性能,减少冗余代码。 Juice提供了丰富的动态SQL支持,通过XML配置方式实现灵活的SQL构建。以下是支持的主要动态SQL元素: 条件判断(if) -------------- ``if`` 元素用于条件判断,根据表达式结果决定是否包含特定的SQL片段: .. code-block:: xml .. tip:: 条件表达式支持各种运算符和函数,如:``&&``、``||``、``!``、``>``, ``<``、``==``、``!=`` 等。 动态WHERE(where) ------------------ ``where`` 元素智能处理WHERE子句,自动添加WHERE关键字并去除多余的AND/OR: .. code-block:: xml .. note:: ``where`` 元素会自动处理以下情况: - 如果where中的条件都不满足,则不会添加where关键字 - 自动去除开头的AND/OR - 确保生成的SQL语法正确 动态SET(set) -------------- ``set`` 元素用于UPDATE语句,自动处理SET子句并去除多余的逗号: .. code-block:: xml update user name = #{name}, age = #{age}, email = #{email}, where id = #{id} .. tip:: ``set`` 元素会自动: - 添加SET关键字 - 去除最后多余的逗号 - 如果没有要更新的字段,会生成有效的SQL 集合遍历(foreach) ------------------- ``foreach`` 元素用于遍历集合,常用于IN条件和批量操作: .. code-block:: xml insert into user (name, age) values (#{user.name}, #{user.age}) .. note:: foreach支持的属性: - ``collection``: 要遍历的集合 - ``item``: 当前遍历的元素 - ``index``: 当前遍历的索引 - ``open``: 开始字符 - ``close``: 结束字符 - ``separator``: 分隔符 条件修饰(trim) ---------------- ``trim`` 元素用于自定义SQL语句的修饰规则: .. code-block:: xml .. tip:: trim的属性说明: - ``prefix``: 要添加的前缀 - ``prefixOverrides``: 要去除的前缀 - ``suffix``: 要添加的后缀 - ``suffixOverrides``: 要去除的后缀 多重条件选择(choose) ---------------------- ``choose`` 元素提供类似switch的条件选择功能: .. code-block:: xml SQL片段复用(sql/include) -------------------------- ``sql`` 和 ``include`` 元素用于SQL片段的定义和复用: .. code-block:: xml id, name, age, email, create_time 跨namespace 引用 ~~~~~~~~~~~~~~~~ 在多个namespace之间,可以使用 ``sql/include`` 元素来引用另一个namespace中的SQL片段。 .. code-block:: xml id, name, age, email, create_time .. tip:: sql 的 id 属性是必须的,但是必须是合法的变量名。 sql 片段参数化 ~~~~~~~~~~~~~~~~ ``sql`` 元素支持参数化,可以通过 ```` 元素传递参数: .. code-block:: xml select * from user where ${field} = #{value} 在上面的例子中,我们通过 元素定义了两个参数 field 和 value,并在 sql 片段中使用它们来动态生成 SQL 语句。 参数作用域和优先级 作用域限制 - bind 标签不仅可以在语句的顶层定义,还可以嵌套在动态 SQL 标签(如 等)内部使用 - 嵌套的 bind 标签具有局部作用域,只在当前标签及其子标签内有效 - 父标签定义的 bind 变量对子标签可见,子标签可直接使用 - 内部作用域定义的变量会覆盖外部作用域的同名变量 参数查找优先级 参数查找时遵循以下优先级顺序: 1. bind 定义的参数 - 优先级最高 2. 传递的参数 - 用户传入的参数 3. 系统内置参数 - 如 _databaseId、_parameter 等 这意味着 bind 定义的参数可以覆盖用户传递的同名参数。 高级用法示例 1. 字符串处理 .. code-block:: xml 2. 数值计算 .. code-block:: xml 3. 复杂对象处理 .. code-block:: xml 4. 作用域与覆盖示例 .. code-block:: xml .. tip:: 动态SQL的最佳实践: 1. 合理使用SQL片段复用,提高维护性 2. 注意条件判断的性能影响 3. 使用参数化查询防止SQL注入 4. 保持SQL语句的可读性 5. 适当添加注释说明复杂的动态SQL逻辑 复杂查询实战 ============ 多条件动态搜索 -------------- 电商商品搜索示例 ~~~~~~~~~~~~~~~~ .. code-block:: xml 用户权限查询示例 ~~~~~~~~~~~~~~~~ .. code-block:: xml 复杂统计查询 ------------ 销售数据统计 ~~~~~~~~~~~~ .. code-block:: xml 动态批量操作 ------------ 批量更新示例 ~~~~~~~~~~~~ .. code-block:: xml UPDATE products SET price = #{product.price}, updated_at = NOW() WHERE id = #{product.id} 条件批量删除 ~~~~~~~~~~~~ .. code-block:: xml DELETE FROM users AND status = #{status} AND created_at < #{createdBefore} AND last_login_at < #{lastLoginBefore} AND id NOT IN #{id} LIMIT 1000 性能优化技巧 ============ 避免常见性能陷阱 ---------------- **1. 避免不必要的条件判断** ❌ **不推荐**: .. code-block:: xml ✅ **推荐**: .. code-block:: xml **2. 合理使用 foreach** ❌ **不推荐**:IN 条件包含过多值 .. code-block:: xml ✅ **推荐**:使用临时表或分批查询 .. code-block:: go // 分批查询 func GetUsersByIds(ctx context.Context, ids []int64) ([]*User, error) { const batchSize = 1000 var allUsers []*User for i := 0; i < len(ids); i += batchSize { end := i + batchSize if end > len(ids) { end = len(ids) } batch := ids[i:end] users, err := queryBatch(ctx, batch) if err != nil { return nil, err } allUsers = append(allUsers, users...) } return allUsers, nil } **3. 避免 N+1 查询问题** ❌ **不推荐**: .. code-block:: go // 先查询所有订单 orders, _ := GetOrders(ctx) // 然后为每个订单查询详情(N+1 问题) for _, order := range orders { items, _ := GetOrderItems(ctx, order.ID) order.Items = items } ✅ **推荐**:使用 JOIN 或 IN 查询 .. code-block:: xml 索引优化建议 ------------ **1. 确保 WHERE 条件使用索引** .. code-block:: xml **2. 避免在索引列上使用函数** ❌ **不推荐**: .. code-block:: xml ✅ **推荐**: .. code-block:: xml 查询优化 -------- **1. 只查询需要的字段** ❌ **不推荐**: .. code-block:: xml ✅ **推荐**: .. code-block:: xml **2. 使用 LIMIT 限制结果集** .. code-block:: xml **3. 合理使用子查询** ❌ **不推荐**:相关子查询 .. code-block:: xml ✅ **推荐**:JOIN 或独立子查询 .. code-block:: xml 批量操作优化 ------------ **1. 使用 batchSize 控制批次大小** .. code-block:: xml INSERT INTO products (name, price, stock) VALUES (#{p.name}, #{p.price}, #{p.stock}) **2. 批量更新优化** 使用 CASE WHEN 进行批量更新: .. code-block:: xml UPDATE products SET price = CASE id WHEN #{p.id} THEN #{p.price} END WHERE id IN #{p.id} 调试和监控 ========== SQL 调试技巧 ------------ **1. 启用调试模式** 全局启用: .. code-block:: xml 单个语句启用: .. code-block:: xml **2. 使用自定义日志中间件** .. code-block:: go type SQLLogger struct { logger *log.Logger } func (m *SQLLogger) QueryContext(stmt juice.Statement, cfg juice.Configuration, next juice.QueryHandler) juice.QueryHandler { return func(ctx context.Context, query string, args ...any) (sql.Rows, error) { start := time.Now() // 记录SQL和参数 m.logger.Printf("[SQL] %s", query) m.logger.Printf("[ARGS] %v", args) rows, err := next(ctx, query, args...) duration := time.Since(start) if err != nil { m.logger.Printf("[ERROR] %v (took %v)", err, duration) } else { m.logger.Printf("[SUCCESS] took %v", duration) } return rows, err } } 性能分析 -------- **1. 慢查询监控** .. code-block:: go type SlowQueryMonitor struct { threshold time.Duration reporter func(query string, duration time.Duration, args []any) } func (m *SlowQueryMonitor) QueryContext(stmt juice.Statement, cfg juice.Configuration, next juice.QueryHandler) juice.QueryHandler { return func(ctx context.Context, query string, args ...any) (sql.Rows, error) { start := time.Now() rows, err := next(ctx, query, args...) duration := time.Since(start) if duration > m.threshold { m.reporter(query, duration, args) } return rows, err } } **2. 查询统计** .. code-block:: go type QueryStats struct { mu sync.RWMutex queryCount map[string]int64 totalTime map[string]time.Duration avgTime map[string]time.Duration } func (s *QueryStats) Record(stmtID string, duration time.Duration) { s.mu.Lock() defer s.mu.Unlock() s.queryCount[stmtID]++ s.totalTime[stmtID] += duration s.avgTime[stmtID] = s.totalTime[stmtID] / time.Duration(s.queryCount[stmtID]) } func (s *QueryStats) Report() { s.mu.RLock() defer s.mu.RUnlock() for stmtID, count := range s.queryCount { fmt.Printf("Statement: %s\n", stmtID) fmt.Printf(" Count: %d\n", count) fmt.Printf(" Total Time: %v\n", s.totalTime[stmtID]) fmt.Printf(" Avg Time: %v\n", s.avgTime[stmtID]) } } 最佳实践总结 ============ 动态 SQL 设计原则 ------------------ 1. **保持简单** - 避免过度复杂的嵌套条件 - 复杂逻辑考虑拆分为多个语句 - 优先使用简单的条件判断 2. **性能优先** - 确保动态生成的 SQL 能使用索引 - 避免全表扫描 - 合理使用 LIMIT 3. **可维护性** - 添加清晰的注释 - 使用有意义的变量名 - 保持一致的代码风格 4. **安全性** - 始终使用参数绑定(#{param}) - 避免使用字符串替换(${param})除非必要 - 验证用户输入 检查清单 -------- 在编写动态 SQL 前,请确认: .. code-block:: text ☐ 是否真的需要动态 SQL? ☐ 条件判断是否合理? ☐ 是否会导致 N+1 查询? ☐ 是否使用了索引? ☐ 是否限制了结果集大小? ☐ 是否使用了参数绑定? ☐ 是否添加了注释? ☐ 是否进行了性能测试? .. tip:: **推荐工具**: - 使用 EXPLAIN 分析查询计划 - 使用 Juice IDEA 插件检查 SQL 语法 - 使用慢查询日志监控性能 - 定期 review 和优化 SQL .. warning:: **常见错误**: - 在循环中执行查询(N+1 问题) - IN 条件包含过多值 - 在索引列上使用函数 - 忽略 NULL 值处理 - 不使用 LIMIT 限制结果