sql null 替换 0_MyBatis#动态SQL(七)

在BaseExecutor中调用query方法时,会触发一个BoundSQL的东西

c1fd4911c881e6832d7bb871b7e7a245.png

除此之外,在BaseStatementHandler中也调用过BoundSQL

397b5baedaae48fe534f006f74096d52.png

那么BoundSQL到底是什么东西?

9bb29414c58b2c718d521d9bf60bad29.png
sql:可执行的语句
parameterMapping:sql中的参数位置是"?",通过parameterMapping进行映射
parameterObject:就是传递过来的参数
additionParameters:对原始参数进行运算后得出的新的参数,例如将数组中的参数打散进行分离成Map形式与参数进行一一对应
metaParameters:操纵parameterObject属性

即,BoundSQL包含了我们所要执行的SQL的所有信息,有了BoundSQL就可以发起SQL调用了

动态SQL的定义:

593e8ec025d17eb02983c89729a98c98.png
在每次执行SQL的时候发生的,即每执行一次SQL都会重新进行一次编译和解析

例如:此时有一个SQL脚本

230b68d6c378220bc8360eae4385e0ed.png
select * from users:静态文本元素
<where><if>:脚本表达式

动态SQL就是去解析静态文本元素和脚本表达式最后形成select语句

在执行脚本解析的时候必须要带上所需的参数才可以生成最终的SQL语句

e029000a81d9ea2fc2d6ccd5c4ad0e85.png

回忆一下SQL脚本语言树

72109cb9bc0ed09855817bc224b366ba.png

在使用动态SQL的时候经常会看到一写表达式,例如:

313d378868f72b5df8cf7ce8ca0fdb50.png

这种表达式就称之为OGNL表达式

OGNL表达式

ceea404ec5a5c97ab21be717b76dc0e8.png
  • 访问属性
public class BoundSqlTest {
    private static Configuration configuration;
    private static SqlSessionFactory factory = null;

    static {
        SqlSessionFactoryBuilder factoryBuilder = new SqlSessionFactoryBuilder();
        factory = factoryBuilder.build(StatementHandlerTest.class.getResourceAsStream("/mybatis-config.xml"));
        configuration = factory.getConfiguration();
        configuration.setLazyLoadTriggerMethods(new HashSet<>());
    }

    @Test
    public void ognlTest() {
        //表达式执行器
        ExpressionEvaluator evaluator = new ExpressionEvaluator();

        Blog blog = Mock.newBlog();

        //1.访问属性
        boolean b = evaluator.evaluateBoolean("id != null && author.name != null", blog);
        System.out.println(b);
    }
}

deaaad2b806cc5fb7ec9a94afa2bb7ab.png

如果将author设置为null,则就会报错

@Test
public void ognlTest() {
    //表达式执行器
    ExpressionEvaluator evaluator = new ExpressionEvaluator();

    Blog blog = Mock.newBlog();
    blog.setAuthor(null);
    //1.访问属性
    boolean b = evaluator.evaluateBoolean("id != null && author.name != null", blog);
    System.out.println(b);
}

b6ae2eb2a69bbd7f74ceaa90ff71c9b9.png
  • 调用方法
@Test
public void ognlTest() {
    //表达式执行器
    ExpressionEvaluator evaluator = new ExpressionEvaluator();

    Blog blog = Mock.newBlog();
    blog.setAuthor(null);

    //2.调用方法
    boolean b1 = evaluator.evaluateBoolean("comments!=null && comments.size()>0", blog);
    System.out.println(b1);
}

8a46b5071e1fb51325e0f5c274bb9b51.png

可以使省略方法的括号

evaluator.evaluateBoolean("comments!=null && !comments.isEmpty", blog);
  • 传递参数
evaluator.evaluateBoolean("comments!=null && comments.get(0).body!=null", blog);
evaluator.evaluateBoolean("comments!=null && comments[0].body!=null", blog);
  • 遍历集合
Iterable<?> comments = evaluator.evaluateIterable("comments", blog);
for (Object comment : comments) {
    System.out.println(comment);
}

66d947580274b27a78aa0b14f36e483f.png

脚本的解析流程

由SqlSource(SQL数据源)编程BoundSql(SQL包)的过程

521da30d9e9beade82f233db8f85ad82.png

前面说过,有了BoundSQL就有了执行SQL所需的全部写信息,所以BoundSQL需要sql语句、执行sql所需的参数、参数和参数值的映射(sql中的"?")

43d10038f18a52e1bbfe16007528084b.png

即,需要将SqlSource编程BoundSQL,转换的时机在每次发起SQL调用的时候都会执行一次

a4c9e618b85e0898eadb85a9b365adb6.png

最后,这个脚本(XML)会变成一个数据源sqlSource

66cb9bb93c05f3b48bc07d83245b20d6.png

Xml形成的SqlSource有几种表现形式:

  • Dynamic SqlSource
每次执行的时候都会进行一次编译,即编译我们对应的脚本(XML)把其变成BoundSQL

但是有的SQL不需要每次都进行编译

b6136bb8fd8dbc2cfb478a3752f70344.png

所以还会存在一个静态编译的数据源:

  • Raw SqlSource
只会编译一次,在初始化数据源的时候其就已经编译好了,编译好之后每次就通过它进行执行

两个数据源在编译之后将把结果存储在Static SqlSource中,最终通过Static SqlSource才能生成BoundSql

此外还存在一种用于第三方驱动包的继承:

  • Provider SqlSource

878d4557aa2432a857cb2fb6c935e719.png

Q:为什么不直接通过Dynamic SqlSource或者Raw SqlSource直接生成BoundSQL?

先看一下StaticSqlSource是什么:

2fb90389a19f42213a4289a2adb42f6a.png

是不是感觉和BoundSql没有什么区别?

49dfc2803a539cc3a1311273e35255fb.png

而且其调用方法也是直接通过new的方式来进行BoundSql的创建

c2b4dcf37fb3c834859e739b0e815ba3.png

再来看一下RawSqlSource:

f081b0892af80c7bb32996a6bbd48c9a.png

DynamicSqlSource:

07cc2ad8420f83f1e88c95617391e4a9.png
其对应的SqlSource也是StaticSqlSource

即,RawSqlSource和DynamicSqlSource都会生成StaticSqlSource

因为RawSqlSource和DynamicSqlSource分别对应静态编译和动态编译,StaticSqlSource的作用就是成为RawSqlSource编译后的载体,用来保存RawSqlSource所生成的StaticSqlSource,那么下次再调用RawSqlSource时就可以直接通过已经编译好的SQL语言生成BoundSQL,如果没有RawSqlSource的话也就不用存在StaticSqlSource

RawSqlSource中的解析作用:

b8609d81d001e0254ef6ae9072e63c2f.png

就是将 #{} 变成“?”,将name的值变为parameterMapping映射类(parameterMapping1和parameterMapping2)

631554656cb649afa815577ba9d11b1a.png
  • DynamicSqlSource

脚本的解析流程

动态解析器:解析脚本,解析后再生成数据源

e5542f0f4b797b22a68dc9cfd0a60c0b.png

SQLNode:XML中每个语句都是一个SQLNode

49bc5cc9974bdece4f35ad853b681971.png

即这些SQLNode就组成了一棵语法树,而DynamicSqlSource就是去执行SqlNode中的一个一个的结点

在解析过程中会有一个解析上下文DynamicContext,其包含了已经成功解析的参数,最后生成BoundSQL

每执行一次脚本,DynamicContext中的参数就会发生一次变更,就相当于一个拼装构成

脚本组成结构

ef60839c73308de37e174575df3e71b7.png
MixedSqlNode:包含多个子Node
StaticTextSqlNode:静态文本,就是纯粹的sql文本,没有任何的表达式
TextSqlNode:表达式文本,例如:select * from ${table_name}
使用了 解释器设计模式
注意:#不是表达式文本,会直接替换成?,而$是表达式文本

其执行过程就是将XML文档转换成对应的SqlNode结点,例:

41438c6a09134b2bea18088d0c8d9089.png

执行过程:

9b1a0cdad356cebc103f9e01b49cf72f.png

下面通过代码的方式来证明:

<!--动态SQL处理-->
<select id="findUser" resultMap="result_user" flushCache="true" databaseId="mysql">
    select * from users
    <where>
        <if test="id!=null">
            and id=#{id}
        </if>
        <if test="name!=null">
            <bind name="like_name" value="'%'+name+'%'"/>
            and name like #{like_name}
        </if>
        <if test="age!=null">
            and age=#{age}
        </if>
    </where>
</select>

首选模拟一下BoundSQL的生成过程:

@Test
public void ifTest(){
    User user  = new User();
    user.setId(1);
    DynamicContext context = new DynamicContext(configuration, user);
    //静态结点逻辑
    new StaticTextSqlNode("select * from users where 1=1").apply(context);

    //if结点逻辑
    IfSqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode("and id!=#{id}"), "id!=null");
    ifSqlNode.apply(context);

    //生成 sql
    System.out.println(context.getSql());
}

a2cf15db0058e1b4ee6c25d8b00ca05c.png

如果将user.setId(1)去掉就不会生成

2eb534da966a0b6a94b325054e9d81df.png

或者也可以改写成:

@Test
public void ifTest(){
    User user  = new User();
    user.setId(1);
    DynamicContext context = new DynamicContext(configuration, user);
    //静态结点逻辑
    new StaticTextSqlNode("select * from users").apply(context);

    //if结点逻辑
    IfSqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode("and id!=#{id}"), "id!=null");

    WhereSqlNode where = new WhereSqlNode(configuration, ifSqlNode);
    where.apply(context);
    //生成 sql
    System.out.println(context.getSql());
}

7cd7064545b684284385fa334682b056.png

where中使用TrimSqlNode将and等条件进行删除(如果多余的话)

  1. 添加where前缀
  2. 溢出执行关键字的前缀和后缀

be189f62a375caf310fcee7dc4474258.png

在进行appendSql时,是将其存储在一个缓存区sqlBuffer中,并不是直接追加到contents中

62e24630c746d5c570e062dc743f73cf.png

当字节点全部执行完时,此时子节点需要拼装的全部Sql都已经在sqlBuffer中,最后在applay时将其一次性加入到DynamicContext中

4301b7f8c77aef230a1715f8ba1b5f6f.png

7a805f2bca6bf399cd29c8e8df71b89f.png
添加前缀,替换掉不需要的值,再把SqlBuffer追加到真正的Context中

可以写一个例子自己进行测试:

@Test
public void ifTest() {
    User user = new User();
    user.setId(1);
    user.setName("yymmdd");
    DynamicContext context = new DynamicContext(configuration, user);
    //静态结点逻辑
    new StaticTextSqlNode("select * from users").apply(context);

    //if结点逻辑
    IfSqlNode ifSqlNode = new IfSqlNode(new StaticTextSqlNode("and id!=#{id}"), "id!=null");
    IfSqlNode ifSqlNode1 = new IfSqlNode(new StaticTextSqlNode("or name=#{name}"), "name!=null");

    MixedSqlNode mixedSqlNode = new MixedSqlNode(Arrays.asList(ifSqlNode, ifSqlNode1));

    WhereSqlNode where = new WhereSqlNode(configuration, mixedSqlNode);
    where.apply(context);
    //生成 sql
    System.out.println(context.getSql());
}

7d23b0446dbee03776f8ef807f063aff.png

可以在 where.apply(context); 处断点进行查看

ForEachSqlNode

<select id="findByIds" resultMap="result_user" flushCache="true">
    select * from users
    where id in
    <foreach collection="list" separator="," item="item" open="(" close=")">
        #{item}
    </foreach>
</select>

测试类:

@Test
public void foreachTest() {
    Object list;
    HashMap<Object, Object> parameter = new HashMap<>();
    parameter.put("list", Arrays.asList(1, 2, 3, 4, 5));
    factory.openSession().selectList("findByIds", parameter);
}

7a15692f7ed09baed2dfa061f32ac8e4.png

dad30a21106eb25cee8aad04f716193f.png

0ee0f8cf7215dbf44a0667805f40c4e9.png

e5a7bd8c5566a8dc3c6de9d060f59f45.png

9e21ebe12c3a12db26b0753a533bdb16.png

最终生成的SQL:

00377755538bfc49f219faa359c43b47.png

在获取参数时,会检查是否在条件参数中,如果在则直接获取

79fc500634257945e3b6edc845c4d97a.png

脚本文件解析过程

70a73f46ee29d737f0d6607d0d05af6b.png

525df4440adb09291a815a0f09f5b396.png
可在org.apache.ibatis.scripting.xmltags.XMLScriptBuilder#parseScriptNode断点查看

a15d1b8cc264ad06f10fee21f7432558.png
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值