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_001
复制代码 SQL效果片段:9.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); } public Max(FieldFn fn) { super(fn); } public Max(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
[code]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 |