Dynamic SQL =========== .. note:: Dynamic SQL is a powerful query technique that generates SQL statements at runtime according to changing conditions. It is particularly useful for conditional search, batch operations, and flexible data access. When used well, it improves reuse and maintainability while reducing redundant SQL. Juice provides rich support for dynamic SQL through XML configuration. The main elements are described below. Conditional Logic with if ------------------------- The ``if`` element includes or skips a SQL fragment according to the result of an expression: .. code-block:: xml .. tip:: Conditional expressions support common operators and functions such as ``&&``, ``||``, ``!``, ``>``, ``<``, ``==``, and ``!=``. Dynamic WHERE with where ------------------------ The ``where`` element manages the ``WHERE`` clause automatically. It adds ``WHERE`` only when needed and strips leading ``AND`` or ``OR``: .. code-block:: xml .. note:: ``where`` automatically handles the following cases: - no ``WHERE`` keyword is emitted when no condition matches - leading ``AND`` and ``OR`` are removed - the generated SQL remains syntactically valid Dynamic SET with set -------------------- The ``set`` element is designed for ``UPDATE`` statements. It manages the ``SET`` clause and removes trailing commas: .. code-block:: xml update user name = #{name}, age = #{age}, email = #{email}, where id = #{id} .. tip:: ``set`` automatically: - adds the ``SET`` keyword - removes the final extra comma - keeps the generated SQL valid Iterating Collections with foreach ---------------------------------- The ``foreach`` element iterates over a collection. It is commonly used for ``IN`` conditions and batch operations: .. code-block:: xml insert into user (name, age) values (#{user.name}, #{user.age}) .. note:: ``foreach`` supports these attributes: - ``collection``: the collection to iterate - ``item``: the current element - ``index``: the current index - ``open``: opening text - ``close``: closing text - ``separator``: separator between items Custom Wrapping with trim ------------------------- The ``trim`` element lets you define custom prefix and suffix handling: .. code-block:: xml .. tip:: ``trim`` attributes: - ``prefix``: text to prepend - ``prefixOverrides``: prefixes to remove - ``suffix``: text to append - ``suffixOverrides``: suffixes to remove Conditional Branching with choose --------------------------------- The ``choose`` element works like a switch statement: .. code-block:: xml Reusing SQL Fragments with sql and include ------------------------------------------ The ``sql`` and ``include`` elements define reusable SQL fragments: .. code-block:: xml id, name, age, email, create_time Cross-Namespace References ~~~~~~~~~~~~~~~~~~~~~~~~~~ You can reference SQL fragments from another namespace: .. code-block:: xml id, name, age, email, create_time .. tip:: The ``id`` attribute on ``sql`` is required and must be a valid identifier. Parameterized SQL Fragments ~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``sql`` fragments can also be parameterized with ``bind``: .. code-block:: xml select * from user where ${field} = #{value} In this example, ``field`` and ``value`` are defined with ``bind`` and then used inside the SQL fragment. Parameter Scope and Precedence ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Scope rules: - ``bind`` can be declared at the top level of a statement or nested inside dynamic tags such as ``if``, ``where``, and ``foreach``. - Nested ``bind`` variables have local scope and are visible only within the current tag and its children. - Variables defined in a parent scope are visible to child tags. - Variables defined in an inner scope override variables with the same name from an outer scope. Parameter lookup precedence: 1. Parameters defined by ``bind`` 2. User-provided parameters 3. Built-in system parameters such as ``_databaseId`` and ``_parameter`` That means a ``bind`` variable can override a user-provided parameter with the same name. Advanced Examples ~~~~~~~~~~~~~~~~~ **1. String processing** .. code-block:: xml **2. Numeric calculation** .. code-block:: xml **3. Complex object handling** .. code-block:: xml **4. Scope override example** .. code-block:: xml .. tip:: Dynamic SQL best practices: 1. Reuse SQL fragments where it improves maintainability. 2. Be aware of the runtime cost of complex conditions. 3. Prefer parameterized queries to avoid SQL injection. 4. Keep SQL readable. 5. Add comments for complex dynamic SQL logic when needed. Real-World Query Patterns ========================= Multi-Condition Search ---------------------- E-commerce Product Search Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: xml User Permission Query Example ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: xml Complex Statistical Queries --------------------------- Sales Statistics Example ~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: xml Dynamic Batch Operations ------------------------ Batch Update Example ~~~~~~~~~~~~~~~~~~~~ .. code-block:: xml UPDATE products SET price = #{product.price}, updated_at = NOW() WHERE id = #{product.id} Conditional Batch Delete ~~~~~~~~~~~~~~~~~~~~~~~~ .. 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 Performance Optimization Tips ============================= Avoid Common Performance Pitfalls --------------------------------- **1. Avoid unnecessary conditions** Not recommended: .. code-block:: xml Recommended: .. code-block:: xml **2. Use foreach carefully** Not recommended when the ``IN`` list is too large: .. code-block:: xml Recommended: split into batches or use a temporary table. .. 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. Avoid N+1 queries** Not recommended: .. code-block:: go orders, _ := GetOrders(ctx) for _, order := range orders { items, _ := GetOrderItems(ctx, order.ID) order.Items = items } Recommended: use ``JOIN`` or batched ``IN`` queries. .. code-block:: xml Index Optimization Suggestions ------------------------------ **1. Ensure WHERE conditions use indexes** .. code-block:: xml **2. Avoid functions on indexed columns** Not recommended: .. code-block:: xml Recommended: .. code-block:: xml Query Optimization ------------------ **1. Select only the fields you need** Not recommended: .. code-block:: xml Recommended: .. code-block:: xml **2. Use LIMIT to bound result sets** .. code-block:: xml **3. Use subqueries carefully** Not recommended: .. code-block:: xml Recommended: .. code-block:: xml Batch Operation Optimization ---------------------------- **1. Control batch size with batchSize** .. code-block:: xml INSERT INTO products (name, price, stock) VALUES (#{p.name}, #{p.price}, #{p.stock}) **2. Optimize bulk updates** Use ``CASE WHEN`` for batch updates: .. code-block:: xml UPDATE products SET price = CASE id WHEN #{p.id} THEN #{p.price} END WHERE id IN #{p.id} Debugging and Monitoring ======================== SQL Debugging Tips ------------------ **1. Enable debug mode** Globally: .. code-block:: xml For a single statement: .. code-block:: xml **2. Add a custom logging middleware** .. 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() 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 } } Performance Analysis -------------------- **1. Monitor slow queries** .. 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. Collect query statistics** .. 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]) } } Best Practices Summary ====================== Dynamic SQL Design Principles ----------------------------- 1. **Keep it simple** - Avoid deeply nested conditions where possible. - Split very complex logic into multiple statements. - Prefer straightforward condition checks. 2. **Prioritize performance** - Make sure generated SQL can use indexes. - Avoid full table scans. - Use ``LIMIT`` where appropriate. 3. **Keep it maintainable** - Add clear comments when needed. - Use meaningful variable names. - Keep formatting consistent. 4. **Keep it safe** - Always prefer parameter binding such as ``#{param}``. - Avoid string replacement such as ``${param}`` unless absolutely necessary. - Validate user input. Checklist --------- Before writing dynamic SQL, confirm the following: .. code-block:: text [ ] Do you really need dynamic SQL here? [ ] Are the conditions reasonable? [ ] Could this introduce an N+1 query pattern? [ ] Are the relevant indexes available? [ ] Is the result set size bounded? [ ] Are parameters safely bound? [ ] Have you added comments where the logic is complex? [ ] Have you performance-tested the query? .. tip:: Recommended tools: - use ``EXPLAIN`` to analyze the query plan - use the Juice IDEA plugin to check SQL syntax - use slow-query logs to monitor performance - review and optimize SQL regularly .. warning:: Common mistakes: - executing queries inside loops - putting too many values into an ``IN`` clause - applying functions to indexed columns - ignoring ``NULL`` handling - forgetting to use ``LIMIT`` where appropriate