获取 Mybatis-Plus Wrapper 生成的SQL语句

最近遇到一个需求:不执行数据库查询,而是把查询的逻辑,也就是 SQL 语句,传递给另一个微服务,让它去查。于是就想到,如果可以用 Mybatis-Plus 拼好的 Wrapper 构造器来生成 SQL,就可以不用在代码里面拼 SQL 语句了。

解决办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@Autowired
SqlSessionFactory sqlSessionFactory;

public void getSelectListSQL() {
// wrapper 拼查询条件
QueryWrapper<TaskUser> ew = new QueryWrapper<>();
ew.lambda()
.select(TaskUser::getTaskId)
.eq(TaskUser::getUserId, "123456")
.like(TaskUser::getNodeName, "123456")
.and(w -> w.likeLeft(TaskUser::getOrgCode, "546"))
.orderByAsc(TaskUser::getCreateTime);

// wrapper 会作为参数传入 mapper
Map<String, Object> params = Collections.singletonMap("ew", ew);

// sqlSessionFactory 从 Spring 容器中获得
Configuration configuration = sqlSessionFactory.getConfiguration();

// 使用类引用+方法名得到 mapper 语句
MappedStatement mappedStatement = configuration.getMappedStatement("cn.beanbang.spi.mapper.TaskUserMapper.selectList");
BoundSql boundSql = mappedStatement.getBoundSql(params);

String sql = getExecuteSql(boundSql, params);
System.out.println(sql);
}

private String getExecuteSql(BoundSql boundSql, Object paramObject) {
// 带有问号占位符的 SQL 语句
String sql = boundSql.getSql();
// 参数信息列表
List<ParameterMapping> paramMappings = boundSql.getParameterMappings();
// MetaObject 是 mybatis 通过表达式取出对象内容的工具
MetaObject metaObject = sqlSessionFactory.getConfiguration().newMetaObject(paramObject);
for (ParameterMapping p : paramMappings) {
String paramName = p.getProperty();
Object paramValue = metaObject.getValue(paramName);
String value = "";
if (paramValue instanceof String) {
value = "'" + paramValue + "'";
} else {
// todo 其他类型的参数的对应的拼接方式
}
sql = sql.replaceFirst("\\?", value);
}
return sql;
}

输出的语句:

1
2
3
4
5
6
SELECT
task_id
FROM TASK_USER
WHERE IS_DELETED=2

AND (user_id = '123456' AND node_name LIKE '%123456%' AND (org_code LIKE '%546')) ORDER BY CREATE_TIME ASC

下面是摸索出结论的过程(心路历程

JDBC

所有的 ORM 框架,最后都是要通过 JDBC 去和数据库交互的。类似这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
try (PreparedStatement ps = conn.prepareStatement("SELECT id, grade, name, gender FROM students WHERE gender=? AND grade=?")) {
ps.setObject(1, "M"); // 注意:索引从1开始
ps.setObject(2, 3);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
long id = rs.getLong("id");
long grade = rs.getLong("grade");
String name = rs.getString("name");
String gender = rs.getString("gender");
}
}
}
}
// 来自 https://www.liaoxuefeng.com/wiki/1252599548343744/1321748435828770

为了避免注入问题,基本上所有的语句执行都会使用 PreparedStatement。而 prepared statement 的执行过程是这样的:

  1. 带占位符的语句被发送到数据库服务器,先被解析成某种数据结构并储存在内存中;
  2. 绑定的参数被发送到服务器;
  3. 整个语句被执行。

所以事实上整个查询的过程中都不会有“最终的”SQL 语句生成,没有办法从 JDBC 的执行过程得到 SQL 语句。

不过,一些数据库驱动的实现是可以得到语句的。比如 MySQL 的 PreparedStatementtoString() 方法可以输出拼接参数的完整语句。但是并不是所有数据库厂商都有去实现的,而我们这个项目也是需要支持多种类型的数据库,所以这样也不是很通用。

所以,我们只能手动拼接语句和参数来得到完整的 SQL 语句了。接下来就是要知道 Mybatis 是怎么调用 JDBC 的了。

MyBatis

MyBatis 的原理是,在 XML 文件中用模板语言编写 SQL 语句,然后在代码里面编写一个和 XML 的参数和返回值相匹配的接口(Mapper)。调用接口,并传递相应的参数,MyBatis 就会把参数和模板语句结合起来,生成最终需要执行的参数和语句,然后调用 PreparedStatement 执行。

1
2
3
4
5
<mapper namespace="org.mybatis.example.BlogMapper">
<select id="selectBlog" resultType="Blog">
select * from Blog where id = #{id}
</select>
</mapper>
1
2
3
try (SqlSession session = sqlSessionFactory.openSession()) {
Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
}

Mybatis 的核心是 SqlSessionFactory 类。执行查询,或者获取配置信息都是从这里开始的。程序启动后,Mapper 文件到了代码里面就加载成了一个个的 MappedStatement 对象,一个 MappedStatement 实例对应着一个 Mapper 文件里的一个查询语句:

1
2
Configuration configuration = sqlSessionFactory.getConfiguration();
MappedStatement mappedStatement = configuration.getMappedStatement("org.mybatis.example.BlogMapper.selectBlog");
可以从 SqlSessionFactory 中获得 MappedStament 的列表

那 MappedStatement 是怎么和我们传入的参数组合,最后生成 PreparedStatement 的呢?经过一段时间顺藤摸瓜地查找,终于找到了处理逻辑所在的地方:DefaultParameterHandler#setParameters

MyBatis 对语句传入参数的地方

boundSql:SQL 语句和参数信息。储存了用问号占位符标记的生成的 SQL 语句,以及每一个参数的类型信息和参数符号表达式(就是用 #{} 包起来的部分);

parameterObject:传入的参数;

MetaObject:这个工具可以读取参数对象,然后根据表达式从参数对象取出对应的值。

有了 BoundSql 和 MetaObject 我们就可以手动把问号 SQL 拼成完整 SQL 了。

最后一个问题:Mybatis-Plus Wrapper 的原理是什么?它是怎么调用 Mybatis 的?

MyBatis-Plus

Mybatis-Plus 全面接管了 Mybatis。结合 Spring 框架,从头到尾都不需要手动加载配置,创建连接了。并且内置了很多方便的增删改查接口,以及条件构造器,可以很方便地进行条件查询。

QueryWrapper 里的内容

通过对生成的 QueryWrapper 断点调试我们可以发现,wrapper 自己本身就是传递给 mapper 的参数。Wrapper 负责的是生成 where 部分的语句和参数。wrapper 得到的 SQL 语句里面的参数是 #{ew.paramNameValuePairs.MPGENVALx} 的形式,很明显引用了自己(ew 即 EntityWrapper,是 MP 旧版本的 Wrapper 类名)。

MPP selectList 查询方法的注入

在项目启动的时候,MP 会找到 selectList 方法的实现类 SelectList,然后把所有的 SQL 片段拼接起来,动态生成一个 MappedStatement,然后放入 mybatis 的 sqlSessionFactory 里面。

假如我们调用了 service.selectList(wrapper),其实也相当于执行了一次 Mybatis 查询,只不过 Mapper 语句是 MP 帮你生成的(BaseMapper 方法注入),传入的参数也是 MP 帮你生成的(在 wrapper 里面)。

这样,完整的链条就串起来了:

  1. SqlSessionFactory + Mapper id => MappedStatement(SQL 模板)
  2. 拼接查询条件 Wrapper(参数)
  3. Wrapper + MappedStatement => BoundSql(占位符 SQL + 参数信息)
  4. BoundSql + Wrapper => 完整 SQL

参见

作者

Lazyb0x

发布于

2022-01-26

更新于

2022-09-14

许可协议

评论