获取 Mybatis-Plus Wrapper 生成的SQL语句
最近遇到一个需求:不执行数据库查询,而是把查询的逻辑,也就是 SQL 语句,传递给另一个微服务,让它去查。于是就想到,如果可以用 Mybatis-Plus 拼好的 Wrapper 构造器来生成 SQL,就可以不用在代码里面拼 SQL 语句了。
解决办法:
1 |
|
输出的语句:
1 | SELECT |
下面是摸索出结论的过程(心路历程:
JDBC
所有的 ORM 框架,最后都是要通过 JDBC 去和数据库交互的。类似这样:
1 | try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) { |
为了避免注入问题,基本上所有的语句执行都会使用
PreparedStatement
。而 prepared statement
的执行过程是这样的:
- 带占位符的语句被发送到数据库服务器,先被解析成某种数据结构并储存在内存中;
- 绑定的参数被发送到服务器;
- 整个语句被执行。
所以事实上整个查询的过程中都不会有“最终的”SQL 语句生成,没有办法从 JDBC 的执行过程得到 SQL 语句。
不过,一些数据库驱动的实现是可以得到语句的。比如 MySQL 的
PreparedStatement
的 toString()
方法可以输出拼接参数的完整语句。但是并不是所有数据库厂商都有去实现的,而我们这个项目也是需要支持多种类型的数据库,所以这样也不是很通用。
所以,我们只能手动拼接语句和参数来得到完整的 SQL 语句了。接下来就是要知道 Mybatis 是怎么调用 JDBC 的了。
MyBatis
MyBatis 的原理是,在 XML 文件中用模板语言编写 SQL 语句,然后在代码里面编写一个和 XML 的参数和返回值相匹配的接口(Mapper)。调用接口,并传递相应的参数,MyBatis 就会把参数和模板语句结合起来,生成最终需要执行的参数和语句,然后调用 PreparedStatement 执行。
1 | <mapper namespace="org.mybatis.example.BlogMapper"> |
1 | try (SqlSession session = sqlSessionFactory.openSession()) { |
Mybatis 的核心是 SqlSessionFactory
类。执行查询,或者获取配置信息都是从这里开始的。程序启动后,Mapper
文件到了代码里面就加载成了一个个的 MappedStatement
对象,一个 MappedStatement 实例对应着一个 Mapper
文件里的一个查询语句:
1 | Configuration configuration = sqlSessionFactory.getConfiguration(); |
那 MappedStatement 是怎么和我们传入的参数组合,最后生成
PreparedStatement
的呢?经过一段时间顺藤摸瓜地查找,终于找到了处理逻辑所在的地方:DefaultParameterHandler#setParameters
。
boundSql
:SQL
语句和参数信息。储存了用问号占位符标记的生成的 SQL
语句,以及每一个参数的类型信息和参数符号表达式(就是用 #{}
包起来的部分);
parameterObject
:传入的参数;
MetaObject
:这个工具可以读取参数对象,然后根据表达式从参数对象取出对应的值。
有了 BoundSql 和 MetaObject 我们就可以手动把问号 SQL 拼成完整 SQL 了。
最后一个问题:Mybatis-Plus Wrapper 的原理是什么?它是怎么调用 Mybatis 的?
MyBatis-Plus
Mybatis-Plus 全面接管了 Mybatis。结合 Spring 框架,从头到尾都不需要手动加载配置,创建连接了。并且内置了很多方便的增删改查接口,以及条件构造器,可以很方便地进行条件查询。
通过对生成的 QueryWrapper 断点调试我们可以发现,wrapper
自己本身就是传递给 mapper 的参数。Wrapper 负责的是生成 where
部分的语句和参数。wrapper 得到的 SQL 语句里面的参数是
#{ew.paramNameValuePairs.MPGENVALx}
的形式,很明显引用了自己(ew 即 EntityWrapper,是 MP 旧版本的 Wrapper
类名)。
在项目启动的时候,MP 会找到 selectList 方法的实现类
SelectList
,然后把所有的 SQL 片段拼接起来,动态生成一个
MappedStatement
,然后放入 mybatis 的 sqlSessionFactory
里面。
假如我们调用了
service.selectList(wrapper)
,其实也相当于执行了一次 Mybatis
查询,只不过 Mapper 语句是 MP 帮你生成的(BaseMapper
方法注入),传入的参数也是 MP 帮你生成的(在 wrapper 里面)。
这样,完整的链条就串起来了:
- SqlSessionFactory + Mapper id => MappedStatement(SQL 模板)
- 拼接查询条件 Wrapper(参数)
- Wrapper + MappedStatement => BoundSql(占位符 SQL + 参数信息)
- BoundSql + Wrapper => 完整 SQL
参见
- MyBatis 原理系列(六)-手把手带你了解 BoundSql 的创建过程 - 掘金 (juejin.cn)
- Java 获取 Mybatis 动态生成的 sql - yinder - 博客园 (cnblogs.com)
- java - MyBatis 3 - get SQL string from mapper - Stack Overflow
- MyBatis-Plus 简介和原理解析 | 码农家园 (codenong.com)
- MyBatis-Plus 的 BaseMapper 实现原理 - 掘金 (juejin.cn)
- 面试官:Mybatis 如何将数据库类型转换成 Java 类型?如何创建 SQL 返回的对象?如何起别名? - 知乎 (zhihu.com)
- java - How can I get the SQL of a PreparedStatement? - Stack Overflow
获取 Mybatis-Plus Wrapper 生成的SQL语句
https://blog.beanbang.cn/2022/01/26/get-sql-statement-from-mybatis-plus/