Dynamic‑SQL2 查询篇:MyBatis 增强利器,让 SQL 像写 Java 一样丝滑

Dynamic‑SQL2 查询篇:MyBatis 增强利器,让 SQL 像写 Java 一样丝滑

dynamic‑sql2 的查询能力设计目标: 写 SQL 要像写 Java 一样自然;复杂查询要像搭积木一样组合;结果映射要像操作集合一样顺滑。

本篇简述了:

  • 基础查询
  • 结果映射
  • 分组 / Map / 分页
  • Join / 子查询 / JSON 表
  • 动态列引用
  • 排序与 SQL 注入防御
  • 忽略列
  • 函数查询
  • 正则匹配条件
  • 动态库表名称(schema/table)机制
  • 分页体系(dynamic‑sql2 / MyBatis / 逻辑分页)

引入依赖

截止至2026-01-21,最新版是0.1.8,项目地址:https://github.com/pengweizhong/dynamic-sql2

xml 复制代码
        <!-- Spring2.x -->
        <dependency>
            <groupId>com.dynamic-sql</groupId>
            <artifactId>dynamic-sql2-spring-boot-starter</artifactId>
            <version>0.1.8</version>
        </dependency>
        <!-- Spring3.x -->
        <dependency>
            <groupId>com.dynamic-sql</groupId>
            <artifactId>dynamic-sql2-spring-boot3-starter</artifactId>
            <version>0.1.8</version>
        </dependency>

repository层注入SqlContext 增删改查都和此对象交互:

java 复制代码
    @Resource
    private SqlContext sqlContext;

1. 基础查询与结果映射

1.1 查询列表

java 复制代码
List<Product> list = sqlContext.select()
        .allColumn()
        .from(Product.class)
        .fetch()
        .toList();

1.2 查询单列(标量)

java 复制代码
LocalDate one = sqlContext.select()
        .column(Product::getCreatedAt)
        .from(Product.class)
        .limit(1)
        .fetch(LocalDate.class)
        .toOne();

1.3 查询单条记录

java 复制代码
Product product = sqlContext.select()
        .allColumn()
        .from(Product.class)
        .where(c -> c.andEqualTo(Product::getProductId, 7))
        .fetch()
        .toOne();

或使用主键快捷方式:

java 复制代码
Product product2 = sqlContext.selectByPrimaryKey(Product.class, 7);

2. toList / toOne / toMap / toGroupingBy

2.1 分组 toGroupingBy

java 复制代码
Map<Integer, HashSet<String>> groupingBy = sqlContext.select()
        .distinct()
        .allColumn()
        .from(User.class)
        .fetch()
        .toGroupingBy(
                User::getUserId,
                user -> user.getName() + "_hello",
                HashSet::new,
                ConcurrentHashMap::new
        );

2.2 分组(带 DTO)

java 复制代码
LinkedHashMap<String, HashSet<Integer>> groupingBy = sqlContext.select()
        .allColumn()
        .from(User.class)
        .limit(10)
        .fetch(User.class)
        .toGroupingBy(
                User::getName,
                User::getUserId,
                HashSet::new,
                LinkedHashMap::new
        );

2.3 toMap(含重复 key 处理)

java 复制代码
Map<Integer, String> map = sqlContext.select()
        .distinct()
        .allColumn()
        .from(User.class)
        .fetch()
        .toMap(
                user -> 123,
                user -> user.getName() + "_hello"
        );

重复 key 会抛异常,可自定义合并策略:

java 复制代码
.toMap(
    ProductView::getProductName,
    v -> v,
    (v1, v2) -> v1
);

3. Join / 子查询 / JSON 表

3.1 多级 join + 别名 (自关联)

java 复制代码
List<Map<String, Object>> list = sqlContext.select()
        .column("d1", DepartmentEntity::getId, "l5Id")
        .column("d2", DepartmentEntity::getId, "l4Id")
        .column("d3", DepartmentEntity::getId, "l3Id")
        .column("d4", DepartmentEntity::getId, "l2Id")
        .column("d5", DepartmentEntity::getId, "l1Id")
        .from(DepartmentEntity.class, "d1")
        .leftJoin(DepartmentEntity.class, "d2", c -> c.andEqualTo(new Column("d1","id"), new Column("d2","parent_id")))
        .leftJoin(DepartmentEntity.class, "d3", c -> c.andEqualTo(new Column("d2","id"), new Column("d3","parent_id")))
        .leftJoin(DepartmentEntity.class, "d4", c -> c.andEqualTo(new Column("d3","id"), new Column("d4","parent_id")))
        .leftJoin(DepartmentEntity.class, "d5", c -> c.andEqualTo(new Column("d4","id"), new Column("d5","parent_id")))
        .where(c -> c.andIn(DepartmentEntity::getId, Arrays.asList(1,2,3)))
        .fetchOriginalMap()
        .toList();

3.2 子查询 join

java 复制代码
List<Map<String, Object>> list = sqlContext.select()
        .allColumn(Product.class)
        .from(Product.class)
        .innerJoin(
                select -> select.allColumn(Product.class)
                        .from(Category.class)
                        .join(Product.class, on -> on.andEqualTo(Category::getCategoryId, Product::getCategoryId))
                        .where(c -> c.andLessThanOrEqualTo(Category::getCategoryId, 10)),
                "t",
                on -> on.andEqualTo(Product::getProductId, bindAlias("t", Product::getProductId))
        )
        .fetchOriginalMap()
        .toList();

3.3 JSON 表展开(JsonTable)

java 复制代码
List<Object> list = sqlContext.select()
        .column("o", Order::getOrderId)
        .column("jt", Product::getProductName)
        .from(Order.class, "o")
        .join(() -> new JsonTable(
                        "o",
                        Order::getOrderDetails,
                        "$.items[*]",
                        JsonColumn.builder()
                                .column("product_name")
                                .dataType("VARCHAR(150)")
                                .jsonPath("$.product")
                                .build()
                ),
                "jt",
                null
        )
        .fetch()
        .toList();

4. 动态列引用 ColumnReference

java 复制代码
List<Product> list = sqlContext.select()
        .column(Product::getProductId)
        .columnReference(columnReference())
        .from(Product.class)
        .fetch()
        .toList();
java 复制代码
AbstractColumnReference columnReference() {
    return ColumnReference.withColumns()
            .column(Product::getProductId)
            .columnReference(columnReference2())
            .column(Product::getProductName);
}

5. 排序与 SQL 注入防御

5.1 链式排序

java 复制代码
List<User> list = sqlContext.select()
        .allColumn()
        .from(User.class, "u")
        .orderBy(true, sortField, SortOrder.DESC)
        .thenOrderBy(false, User::getUserId)
        .thenOrderBy(true, User::getName)
        .fetch()
        .toList();

5.2 ORDER BY 注入测试

java 复制代码
sqlContext.select()
        .allColumn()
        .from(User.class)
        .orderBy("user_id; drop table users; --", SortOrder.DESC)
        .fetch()
        .toList();

框架会拒绝非法字段名,抛出异常,避免注入。

6. 忽略列 ignoreColumn

java 复制代码
List<?> list = sqlContext.select()
        .allColumn()
        .ignoreColumn(TempUserEntity::getName)
        .ignoreColumn(TempDeptEntity::getName)
        .from(TempUserEntity.class)
        .join(TempDeptEntity.class, on -> on.andEqualTo(TempUserEntity::getId, TempDeptEntity::getId))
        .fetch()
        .toList();

7. 日期函数 DateFormat / Now

java 复制代码
YearMonth yearMonth = sqlContext.select()
        .column(new DateFormat(new Now(), "%Y-%m"))
        .from(Dual.class)
        .fetch(YearMonth.class)
        .toOne();

8. 正则匹配 andMatches(扩展点)

java 复制代码
List<User> list = sqlContext.select()
        .allColumn()
        .from(User.class)
        .where(c -> c.andMatches(User::getEmail, ".*@gmail\\.com"))
        .fetch()
        .toList();

9. 动态库表名称(schema/table)

dynamic‑sql2@Table 支持占位符解析,可动态:

  • schema
  • table
  • alias
  • dataSourceName

9.1 动态 schema

从0.1.8起,自定义值库表解析器,这在同一实例相似业务下跨库时不同的命令库表命名规则时非常有用,不会影响查询速度。

java 复制代码
@Table(schema = "${tenant.schema:user_center}", name = "t_user")

配置:

properties 复制代码
tenant.schema = tenant_001

SQL效果片段:

sql 复制代码
FROM tenant_001.t_user

9.2 动态表名(含默认值)

java 复制代码
@Table(name = "${tenant.table.user:t_user}")

9.3 动态数据源(最高优先级)

java 复制代码
@Table(dataSourceName = "ds_user")

9.4 全局alias

java 复制代码
@Table(name = "t_user", alias = "u")

10. 分页体系(PageHelper)

dynamic-sql2内置了分页支持的查询

10.1 dynamic‑sql2 分页

java 复制代码
PageInfo<List<User>> pageInfo = PageHelper.of(1, 10)
        .selectPage(() -> sqlContext.select()
                .allColumn()
                .from(User.class)
                .fetch()
                .toList());

10.2 MyBatis 分页

java 复制代码
PageInfo<List<User>> pageInfo = PageHelper.ofMybatis(1, 10)
        .selectPage(() -> sqlContext.select()
                .allColumn()
                .from(User.class)
                .fetch()
                .toList());

Dynamic-SQL2支持mybatis的分页,但是需要引入拓展包:

xml 复制代码
<!-- Source: https://mvnrepository.com/artifact/com.dynamic-sql/dynamic-sql2-extension -->
<dependency>
    <groupId>com.dynamic-sql</groupId>
    <artifactId>dynamic-sql2-extension</artifactId>
    <version>0.1.6</version>
    <scope>compile</scope>
</dependency>

该拓展包除了支持Mybatis分页外,和其映射规则也是完全兼容。

10.3 applyWhere(实验性)

该场景有时会遇到类似情况:有的依赖jar有自己独立的逻辑体系,但是又想修改其内部SQL,在不改变内部逻辑的情况下,在外部尝试修改SQL语句。目前只是实验阶段,有足够的场景场景支撑和更多的测试后,才会Release该特性。

java 复制代码
PageInfo<List<User>> pageInfo = PageHelper.of(1, 3)
        .applyWhere(c -> c.andGreaterThanOrEqualTo(User::getAge, 18))
        .selectPage(
    			//假设这是无法修改/不允许更改的内部SQL,通常是jar的形式提供
    			() -> sqlContext.select()
                .allColumn()
                .from(User.class)
                .fetch()
                .toList());

10.4 逻辑分页(集合内存分页)

java 复制代码
PageInfo<List<Integer>> pageInfo = PageHelper.ofLogic(2, 3)
        .selectPage(Arrays.asList(1,2,3,4,5,6,7));

11. 分页 + 动态库表名称示例

java 复制代码
@Table(
    schema = "${tenant.schema:user_center}",
    name = "${tenant.table.user:t_user}",
    alias = "u"
)
public class User {}

分页查询:

java 复制代码
PageInfo<List<User>> pageInfo = PageHelper.of(1, 10)
        .selectPage(() -> sqlContext.select()
                .allColumn()
                .from(User.class)
                .fetch()
                .toList());

最终 SQL:

java 复制代码
SELECT u.* 
FROM tenant_001.user_2025 u 
LIMIT 10 OFFSET 0

拓展

自定义函数

对于Dynamic-SQL2没有提供的函数,如何自定义呢?非常简单,继承ColumnFunctionDecorator抽象类重写getFunctionToString方法即可,然后代码中就可以引用了。

比如已存在的max函数为例:

java 复制代码
/*
 * Copyright (c) 2024 PengWeizhong. All Rights Reserved.
 *
 * This source code is licensed under the MIT License.
 * You may obtain a copy of the License at:
 * https://opensource.org/licenses/MIT
 *
 * See the LICENSE file in the project root for more information.
 */
package com.dynamic.sql.core.column.function.windows.aggregate;


import com.dynamic.sql.core.FieldFn;
import com.dynamic.sql.core.Version;
import com.dynamic.sql.core.column.function.AbstractColumFunction;
import com.dynamic.sql.core.column.function.ColumnFunctionDecorator;
import com.dynamic.sql.core.column.function.windows.WindowsFunction;
import com.dynamic.sql.enums.SqlDialect;
import com.dynamic.sql.utils.ExceptionUtils;
import com.dynamic.sql.model.TableAliasMapping;

import java.util.Map;


public class Max extends ColumnFunctionDecorator implements AggregateFunction, WindowsFunction {

    public Max(AbstractColumFunction delegateFunction) {
        super(delegateFunction);
    }

    public <T, F> Max(FieldFn<T, F> fn) {
        super(fn);
    }

    public <T, F> Max(String tableAlias, FieldFn<T, F> fn) {
        super(tableAlias, fn);
    }

    @Override
    public String getFunctionToString(SqlDialect sqlDialect, Version version, Map<String, TableAliasMapping> aliasTableMap) throws UnsupportedOperationException {
        if (sqlDialect == SqlDialect.ORACLE) {
            return "MAX(" + delegateFunction.getFunctionToString(sqlDialect, version, aliasTableMap) + ")".concat(appendArithmeticSql(sqlDialect, version));
        }
        if (sqlDialect == SqlDialect.MYSQL) {
            return "max(" + delegateFunction.getFunctionToString(sqlDialect, version, aliasTableMap) + ")".concat(appendArithmeticSql(sqlDialect, version));
        }
        throw ExceptionUtils.unsupportedFunctionException("max", sqlDialect);
    }
}

之后在代码中直接引用该类:

java 复制代码
    @Test
    void testMax() {
        Integer max = sqlContext.select()
                .column(new Max(Product::getProductId))
                .from(Product.class)
                .fetch(Integer.class)
                .toOne();
        System.out.println(max);
    }

打印的SQL

log 复制代码
2026-01-21 13:27:03 [main] DEBUG com.dynamic.sql.core.database.SqlDebugger - dataSource -->     Preparing: select max(`p`.`product_id`) as productId from `dynamic_sql2`.`products` as `p`
2026-01-21 13:27:03 [main] DEBUG com.dynamic.sql.core.database.SqlDebugger - dataSource -->    Parameters: 
2026-01-21 13:27:03 [main] DEBUG com.dynamic.sql.core.database.SqlDebugger - dataSource <--         Total: 1

AggregateFunctionWindowsFunction标识函数的分类,因为有些场景下函数嵌套使用时,会要求是窗口函数或必须是字符串函数,因此声明类型更加符合开发规范,如果自定义的话,可以不用关心具体的函数分类,直接继承ColumnFunctionDecorator即可。

目前定义的函数分类接口:

  • AggregateFunction : 聚合函数
  • ScalarFunction : 标量函数

Max函数依赖的全部体系如图所示:

国产数据库

对于国产数据库,通常都会支持和兼容mysql语法,因此通常不用太担心不兼容的问题。但是dynamic-sql2启动时会检测受支持的数据库,对于不支持的数据库会支持报错,只要你确认当前所使用的数据库提供商兼容Mysql,那么就可以完全使用dynamic-sql2

推荐一款好用的 IDEA Mybatis 插件

最喜欢的特性之一是在控制台可以将打印的SQL直接合并为可执行的SQL语句,在开发环境中特别有用!

插件主页:https://plugins.jetbrains.com/plugin/9837-mybatiscodehelperpro

🎉 完结撒花,欢迎吐槽🎉

相关推荐
Remember_9932 小时前
【数据结构】深入理解优先级队列与堆:从原理到应用
java·数据结构·算法·spring·leetcode·maven·哈希算法
Leo July2 小时前
【Java】Spring Cloud 微服务生态全解析与企业级架构实战
java·spring cloud
Marktowin2 小时前
SpringBoot项目的国际化流程
java·后端·springboot
墨雨晨曦883 小时前
2026/01/20 java总结
java·开发语言
汤姆yu3 小时前
基于springboot的直播管理系统
java·spring boot·后端
a努力。3 小时前
虾皮Java面试被问:分布式Top K问题的解决方案
java·后端·云原生·面试·rpc·架构
黎雁·泠崖3 小时前
Java字符串入门:API入门+String类核心
java·开发语言·python
leikooo3 小时前
Spring AI 工具调用回调与流式前端展示的完整落地方案
java·spring·ai·ai编程
vx1_Biye_Design3 小时前
基于web的物流管理系统的设计与实现-计算机毕业设计源码44333
java·前端·spring boot·spring·eclipse·tomcat·maven