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
AggregateFunction和WindowsFunction标识函数的分类,因为有些场景下函数嵌套使用时,会要求是窗口函数或必须是字符串函数,因此声明类型更加符合开发规范,如果自定义的话,可以不用关心具体的函数分类,直接继承ColumnFunctionDecorator即可。
目前定义的函数分类接口:
- AggregateFunction : 聚合函数
- ScalarFunction : 标量函数
Max函数依赖的全部体系如图所示:
国产数据库
对于国产数据库,通常都会支持和兼容mysql语法,因此通常不用太担心不兼容的问题。但是dynamic-sql2启动时会检测受支持的数据库,对于不支持的数据库会支持报错,只要你确认当前所使用的数据库提供商兼容Mysql,那么就可以完全使用dynamic-sql2!
推荐一款好用的 IDEA Mybatis 插件
最喜欢的特性之一是在控制台可以将打印的SQL直接合并为可执行的SQL语句,在开发环境中特别有用!
插件主页:https://plugins.jetbrains.com/plugin/9837-mybatiscodehelperpro