动态 SQL 是 MyBatis 的强大特性之一。
而#{}和${}这两个语法是为了动态传递参数而存在的,是Mybatis实现动态SQL的基础。虽然他们都是为了进行动态传参,但是在编译过程,安全性,使用场景方面却有着许多的不同。
SQL注入式攻击
在开启下面的文章之前,先来介绍一下什么叫做SQL注入式攻击。
所谓SQL注入式攻击,就是攻击者把SQL命令插入到Web表单的输入域或页面请求的查询字符串,欺骗服务器执行恶意的SQL命令。
在某些表单中,用户输入的内容直接用来构造(或者影响)动态SQL命令,或作为存储过程的输入参数,这类表单特别容易受到SQL注入式攻击。
下面是一个SQL注入式攻击的简单案例,相信你一定在不止一个地方看到过。
这种注入方式即为用户通过在输入密码的时候输入一段必定能查询出数据的SQL语句进行不友好的数据访问的一种情况。可以发现用户虽然密码输入的不正确但是依旧可以登录成功。因此我们需要解决这种危险情况。
好了,从现在开始介绍#{}和${}的区别。
编译过程
#{}使用预编译处理,是占位符。
执行顺序为:动态解析->预编译->执行
数据库得到从客户端发送来的SQL语句之后,需要进行词法和语义解析,优化SQL语句,制定执行计划。这需要花费一些时间。但是很多情况,我们的一条SQL语句可能会反复执行,或者每次执行的时候只有个别的值不同,例如select的where子句值不同,update的set子句值不同,insert子句的values值不同而已。
如果每次都需要经过上面的词法语义解析、语句优化、制定执行计划等,则每次接收到一个SQL语句,都要进行一次编译,那么明显会降低数据库的效率。为了解决问题,于是就有了预编译,预编译语句就是将这类语句中的值使用占位符替代,可以理解为将SQL语句变为了一个函数,只需要传递对应的参数即可。一次编译、多次运行,省去了解析优化等过程,提高了性能。mybatis默认对所有SQL进行预编译处理。
预编译语句被数据库的编译器编译后的执行代码将被缓存下来,那么下次调用的时候只要是相同的预编译语句就不再需要进行编译,只要将参数直接传入编译过的语句中即可,这样一个预编译语句就变成了一个类似于函数的东西。
也正是因为预编译后的SQL语句类似于一个函数,因此,其后注入的参数都不会再进行SQL编译,也就是说其后诸如进来的参数系统不会认为他是一条SQL命令,而是理解为一个参数,参数中的or或者and就不再被理解为是SQL中的语法关键字了,而是被理解为纯数据处理。
当然,并不是所有预编译语句都一定会被缓存,数据库本身会用一种策略进行判断(内部机制)。
预编译是通过PreparedStatement和占位符来实现的。PreparedStatement是Statement的子接口,名为预编译声明,可以使用PreparedStatement来替代Statement。使用PreparedStatement能够防止SQL攻击,提高代码可读性,可维护性和效率。而PreparedStatement因为重复使用了预编译语句,通过给予其不同的参数来重复的使用它,从而提高了效率。并且PreparedStatement会对参数进行过滤,不合法的参数将会被过滤掉,提高了安全性。
预编译是提前对SQL语句进行预编译,而其后注入的参数将不会再进行SQL编译。我们知道,SQL注入是发生在编译的过程中,因为恶意注入了某些特殊字符,最后被编译成了恶意的执行操作。而预编译机制则可以很好地防止SQL注入。
${}使用字符串替换,是拼接符。
执行顺序:动态解析->编译->执行
${} 仅仅为一个单纯的字符串替换符号而已,在动态SQL解析阶段将会被变量替换,而 $ {} 的变量替换的阶段是在动态SQL的解析阶段,因此你在${}输入的数据如果包含or或and这种SQL语法关键字,那么最终这一段变量可能会被直接拼接到SQL语句中去并被执行。例如上面SQL注入式攻击中的图片就是这种情况。用户输入的参数直接替换掉了 ${} 这一拼接符,从而导致了SQL注入式攻击。
安全性
#{}不会改变原有的SQL规则,占位符 “?”会被直接完整的替换为参数,因此可以防止SQL注入问题。
${}使用直接字符串拼接技术,因此会出现SQL注入问题。
自动加单引号
#{}将会自动为对应的变量添加上单引号。
${}不会自动为对应的变量添加上单引号。
先看第一种情况,这种情况下面,我们成功的查询出来了数据,把我们上面的理论带入,那么其实我们向数据库发起的SQL语句为:
select *from tb_brandwhere brand_name like '%' '华为' '%';
可以发现上面的SQL语句是可以正常执行的,原因也就是因为使用#{}的方式会在执行SQL语句的时候在参数两边添加上单引号。
而如果我们换为使用${},那么就会失败。
其对应的SQL语句为:
select *from tb_brandwhere brand_name like '%' 华为 '%';
多参数传递
大部分情况,我们的CRUD操作都会不止只有一个参数,而会出现多参数的情况,那么面对多个参数,Mybatis默认使用0,1…来代替传参,根据版本不同,也可以使用arg0,arg1,arg2,param1,param2,param3来代替,具体情况可以根据报错提示来编写。
注意,使用arg则从0开始,使用param则从1开始。
也可以使用@Param注解对参数名称进行设定。
public List<Brand> selectByCondition(@Param("one") String name1, @Param("two") String name2,@Param("three") int status);
这种情况,则只要你把对应的参数名称通过@Param注解设定后即可一一对应。
$ {}的使用场景
$ {} 也并不是一无是处,在某些情况下我们必须使用$ {}。
首先就是排序的场景,也就是使用order by语句的时候必须使用${}。
原因是因为order by语句要求其后跟着的是字段名称。而上面我们提到过,使用#{}会在字符串头尾添加单引号,而这种情况下排序将会失效。
情况如下:
使用#{}进行排序查询的时候,其对应的SQL语句如下:
select *from tb_userorder by 'id' desc;
而如果使用${}就不会出现这种问题。
还有一种情况我们也必须使用$ {},那就是表名也不确定的时候,需要通过参数传递的时候,那么我们也必须使用${},原因和order by语句一样。
总结
1:尽量使用#{},只要能使用#{}解决,尽量使用#{}
2:表名作为参数,使用order by排序的时候使用${}
3:多参数的时候尽量使用@Param注解,@Param注解作用为给参数命名,命名后即可根据名字得到对应的参数值。