鞍注塔 发表于 2026-1-21 19:40:02

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
                            com.dynamic-sql            dynamic-sql2-spring-boot-starter            0.1.8                                    com.dynamic-sql            dynamic-sql2-spring-boot3-starter            0.1.8      在repository层注入SqlContext增删改查都和此对象交互:
    @Resource    private SqlContext sqlContext;1. 基础查询与结果映射

1.1 查询列表

List list = sqlContext.select()      .allColumn()      .from(Product.class)      .fetch()      .toList();1.2 查询单列(标量)

LocalDate one = sqlContext.select()      .column(Product::getCreatedAt)      .from(Product.class)      .limit(1)      .fetch(LocalDate.class)      .toOne();1.3 查询单条记录

Product product = sqlContext.select()      .allColumn()      .from(Product.class)      .where(c -> c.andEqualTo(Product::getProductId, 7))      .fetch()      .toOne();或使用主键快捷方式:
Product product2 = sqlContext.selectByPrimaryKey(Product.class, 7);2. toList / toOne / toMap / toGroupingBy

2.1 分组 toGroupingBy

Map groupingBy = sqlContext.select()      .distinct()      .allColumn()      .from(User.class)      .fetch()      .toGroupingBy(                User::getUserId,                user -> user.getName() + "_hello",                HashSet::new,                ConcurrentHashMap::new      );2.2 分组(带 DTO)

LinkedHashMap 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 处理)

Map map = sqlContext.select()      .distinct()      .allColumn()      .from(User.class)      .fetch()      .toMap(                user -> 123,                user -> user.getName() + "_hello"      );重复 key 会抛异常,可自定义合并策略:
.toMap(    ProductView::getProductName,    v -> v,    (v1, v2) -> v1);3. Join / 子查询 / JSON 表

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

List 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

List 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)

List 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

List list = sqlContext.select()      .column(Product::getProductId)      .columnReference(columnReference())      .from(Product.class)      .fetch()      .toList();AbstractColumnReference columnReference() {    return ColumnReference.withColumns()            .column(Product::getProductId)            .columnReference(columnReference2())            .column(Product::getProductName);}5. 排序与 SQL 注入防御

5.1 链式排序

List 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 注入测试

sqlContext.select()      .allColumn()      .from(User.class)      .orderBy("user_id; drop table users; --", SortOrder.DESC)      .fetch()      .toList();框架会拒绝非法字段名,抛出异常,避免注入。
6. 忽略列 ignoreColumn

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

YearMonth yearMonth = sqlContext.select()      .column(new DateFormat(new Now(), "%Y-%m"))      .from(Dual.class)      .fetch(YearMonth.class)      .toOne();8. 正则匹配 andMatches(扩展点)

List 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起,自定义值库表解析器,这在同一实例相似业务下跨库时不同的命令库表命名规则时非常有用,不会影响查询速度。
@Table(schema = "${tenant.schema:user_center}", name = "t_user")配置:
tenant.schema = tenant_001SQL效果片段:
FROM tenant_001.t_user9.2 动态表名(含默认值)

@Table(name = "${tenant.table.user:t_user}")9.3 动态数据源(最高优先级)

@Table(dataSourceName = "ds_user")9.4 全局alias

@Table(name = "t_user", alias = "u")10. 分页体系(PageHelper)

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

PageInfo pageInfo = PageHelper.of(1, 10)      .selectPage(() -> sqlContext.select()                .allColumn()                .from(User.class)                .fetch()                .toList());10.2 MyBatis 分页

PageInfo pageInfo = PageHelper.ofMybatis(1, 10)      .selectPage(() -> sqlContext.select()                .allColumn()                .from(User.class)                .fetch()                .toList());Dynamic-SQL2支持mybatis的分页,但是需要引入拓展包:
    com.dynamic-sql    dynamic-sql2-extension    0.1.6    compile该拓展包除了支持Mybatis分页外,和其映射规则也是完全兼容。
10.3 applyWhere(实验性)

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

PageInfo pageInfo = PageHelper.ofLogic(2, 3)      .selectPage(Arrays.asList(1,2,3,4,5,6,7));11. 分页 + 动态库表名称示例

@Table(    schema = "${tenant.schema:user_center}",    name = "${tenant.table.user:t_user}",    alias = "u")public class User {}分页查询:
PageInfo pageInfo = PageHelper.of(1, 10)      .selectPage(() -> sqlContext.select()                .allColumn()                .from(User.class)                .fetch()                .toList());最终 SQL:
SELECT u.* FROM tenant_001.user_2025 u LIMIT 10 OFFSET 0拓展

自定义函数

对于Dynamic-SQL2没有提供的函数,如何自定义呢?非常简单,继承ColumnFunctionDecorator抽象类重写getFunctionToString方法即可,然后代码中就可以引用了。
比如已存在的max函数为例:
/* * 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);    }    publicMax(FieldFn fn) {      super(fn);    }    publicMax(String tableAlias, FieldFn fn) {      super(tableAlias, fn);    }    @Override    public String getFunctionToString(SqlDialect sqlDialect, Version version, Map 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);    }}之后在代码中直接引用该类:
    @Test    void testMax() {      Integer max = sqlContext.select()                .column(new Max(Product::getProductId))                .from(Product.class)                .fetch(Integer.class)                .toOne();      System.out.println(max);    }打印的SQL
2026-01-21 13:27:03 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 DEBUG com.dynamic.sql.core.database.SqlDebugger - dataSource -->    Parameters: 2026-01-21 13:27:03 DEBUG com.dynamic.sql.core.database.SqlDebugger - dataSource

峰襞副 发表于 2026-2-2 15:31:33

感谢分享,学习下。

枢覆引 发表于 2026-2-9 11:48:02

感谢,下载保存了

豺独 发表于 2026-2-10 17:08:26

用心讨论,共获提升!

杆树 发表于 2026-2-11 06:21:50

谢谢楼主提供!

褐洌 发表于 2026-2-11 20:23:27

分享、互助 让互联网精神温暖你我

睁扼妤 发表于 2026-2-12 10:03:51

鼓励转贴优秀软件安全工具和文档!

左丘雅秀 发表于 2026-3-7 11:21:45

不错,里面软件多更新就更好了

喜及眩 发表于 2026-3-8 12:54:45

用心讨论,共获提升!

诸婉丽 发表于 2026-3-9 04:49:51

收藏一下   不知道什么时候能用到

喳谍 发表于 2026-3-9 10:11:38

懂技术并乐意极积无私分享的人越来越少。珍惜

劳暄美 发表于 2026-3-11 07:51:18

鼓励转贴优秀软件安全工具和文档!

亢安芙 发表于 2026-3-12 04:18:09

谢谢分享,试用一下
页: [1]
查看完整版本: Dynamic‑SQL2 查询篇:MyBatis 增强利器,让 SQL 像写 Java 一样丝滑