MyBatis 动态 SQL 为什么能如此灵活?

如果你写过 JDBC,应该都有过这样的经历,领导突然来了一个需求:支持按姓名查询

于是你写了一条 SQL:

select * from user where name = ?

过两天需求又变了,支持按年龄查询。

于是又写:

select * from user where age = ?

再过几天,支持姓名和年龄同时查询。

于是继续写:

select * from user
where name = ?
and age = ?

刚开始觉得没什么,但随着查询条件越来越多。

问题就来了,如果有 5 个查询条件,你要写多少种 SQL?


后来我们开始使用 MyBatis。

神奇的事情出现了。

一个 SQL 竟然就解决了所有问题。

<select id="queryUser">

    select * from user

    <where>

        <if test="name != null">
            and name = #{name}
        </if>

        <if test="age != null">
            and age = #{age}
        </if>

    </where>

</select>

传不同参数,执行不同 SQL,仿佛 SQL 会自己变化一样。

那么问题来了,MyBatis 到底是怎么做到的?


很多人以为 MyBatis 在拼字符串

第一次接触动态 SQL 时,很多人都会觉得:这不就是字符串拼接吗?

比如:

String sql = "select * from user";

if(name != null){
    sql += " and name = ?";
}

确实能实现,但问题也很明显,SQL 越复杂,代码越难维护,而且各种 if、else 拼在一起,很容易出错,所以 MyBatis 采用了另外一种设计。


MyBatis 保存的不是 SQL

很多人以为,MyBatis 启动时,会把下面这段 SQL 保存起来

<if test="name != null">
    and name = #{name}
</if>

实际上不是

MyBatis 保存的是:

SQL 的生成规则。

换句话说,它保存的不是结果而是过程。


SQL 在 MyBatis 眼里是一棵树

例如下面这段动态 SQL。

<where>

    <if test="name != null">
        and name = #{name}
    </if>

    <if test="age != null">
        and age = #{age}
    </if>

</where>

在 MyBatis 看来,它不是一段字符串,而是一棵树。

大概类似这样:

where
 ├── if(name)
 └── if(age)

执行时MyBatis 会从上往下遍历,看看哪些节点满足条件,满足就拼进去,不满足就跳过,最后生成真正执行的 SQL


SQL 是什么时候生成的?

答案是:执行时

例如:

userMapper.query(dto);

调用发生以后,MyBatis 才开始判断:

name != null ?

如果成立。

拼接:

and name = ?

继续判断:

age != null ?

如果成立。

再拼接:

and age = ?

最终得到:

select * from user
where name = ?
and age = ?

所以动态 SQL 的本质并不是修改 SQL。

而是:

根据参数动态生成 SQL。


test 为什么能执行?

很多人第一次看到:

<if test="name != null">

都会产生一个疑问,这里明明是字符串,为什么能判断真假?

原因是 MyBatis 引入了一套表达式引擎。

叫做:

OGNL

执行时。

MyBatis 会把参数对象放进去。

然后计算:

name != null

最终得到 true 或 false。

从而决定当前节点是否生效。


为什么 foreach 这么好用?

开发中最常见的其实不是 if。

而是:

<foreach>

例如:

<select id="selectBatch">

    select * from user

    where id in

    <foreach collection="ids"
             item="id"
             open="("
             separator=","
             close=")">

        #{id}

    </foreach>

</select>

传入:

[1,2,3]

最终生成:

select * from user
where id in (?,?,?)

传入:

[1,2,3,4,5]

最终生成:

select * from user
where id in (?,?,?,?,?)

这也是动态 SQL 的能力。

根据参数动态生成最终 SQL。


看一眼源码

动态 SQL 的核心入口:

DynamicSqlSource

执行查询时。

最终会调用:

getBoundSql()

这里会遍历整棵 SQL 树,判断哪些节点需要参与生成,最后得到真正执行的 SQL,然后交给 Executor 去执行。


总结

很多人以为:

<if>

<where>

<foreach>

只是 MyBatis 提供的几个标签,实际上它们背后是一套非常经典的设计。

整个过程本质上是:

动态SQL
    ↓
解析成SQL树
    ↓
运行时判断条件
    ↓
生成最终SQL
    ↓
执行SQL

所以:

MyBatis 动态 SQL 的核心并不是字符串拼接,而是把 SQL 抽象成一棵可执行的节点树,在运行期间根据参数动态生成最终 SQL。

这也是为什么十几年过去了,MyBatis 的动态 SQL 依然没能轻易取代


上一篇:

《查询结果为什么能自动变成 Java 对象?》

下一篇:

《MyBatis 插件为什么这么强大?》


你第一次接触 MyBatis 动态 SQL 的时候,有没有觉得 <if><foreach> 特别神奇?

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐