开发中经常使用mybatis的orm框架对数据库进行相关操作,mybatis最大的好处就是可以使用动态sql,即在mybatis对sql进行预编译之前会对sql语句进行动态解析,处理之后再进行预编译从而到达DBMS执行sql语句。而我们开发中经常用到的动态传参#{param}和${param}在动态解析时会有可能产生不同的sql语句,下面我们就来讨论一下如何正确使用#和$。
预编译
先来熟悉一下预编译,所谓预编译理解下来即:数据库驱动在发送sql和参数到DBMS(数据库管理软件,暂时这么理解吧)之前,先对sql语句进行编译处理,之后DBMS则可以直接对sql进行处理,不需要再次编译,提高了性能。这一点mybatis 默认情况下,将对所有的 sql 进行预编译处理。
使用预编译可以带来哪些好处?
JDBC中使用PreparedStatement来抽象预编译语句,从而使用达到预编译效果。
1:预编译阶段可以对sql语句进行优化。
预编译可以将多个操作步骤合并成一个步骤,一般而言,越复杂的sql,编译程度也会复杂,难度大,耗时,费性能,而预编译可以合并这些操作,预编译之后DBMS可以省去编译直接运行sql。
2:预编译语句可以重复利用。
把一个 sql 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个sql,可以直接使用这个缓存的 PreparedState 对象。有兴趣的可以查看mysql驱动包里面的ConnectionImpl类是如何操作预编译的,这里提一下缓存PreparedStatement,在执行预编译时会先去判断是否存在缓存,如果存在则对参数清空,绑定新的参数。如果不存在则调用数据库进行预编译处理生成一个PreparedStatement对象。
mybatis动态解析
关于动态解析的原理,可以参考这篇文章,写的非常详细mybatis动态解析原理。
理解下来当我们在xml文件中写入动态sql之后,调用相关封装类进行解析处理,最终生成一个BounSql,该类包括sql语句,参数,参数源数据等参数。
#{}和${}在动态解析时的区别?
mybatis动态解析时,对于变量的值替换发生在不同阶段。
#{ } 解析为一个 JDBC 预编译语句(prepared statement)的参数标记符,变量替换完成在DBMS中。
${ } 仅仅为一个纯碎的 string 替换,在动态 SQL 解析阶段将会进行变量替换,DBMS拿到的即为执行的sql。
这里需要说明的是并不是使用${}一定会产生注入,或是一定会被显示替换变量。
例如当${}使用在字段的动态变量时,mybatis在动态解析时是会替换成一个string字符串的,不是显示的字段名字。select * from user where name = ${name};传入name为auth,则在动态解析时也会生成select * from user where name = 'auth';从而DBMS进行处理sql。这里${}和#{}可以达到相同的效果,区别就是#{}会生成一个预编译sql给DBMS,在DBMS完成变量的替换。
然而如果${}的位置发生在from后面或是order by(开发中经常会用到动态根据字段排序),这时候会显式的使用变量;select * from user where name='auth' order by ${column};select * from ${tableName} where name=#{name};这类的${}会进行显式处理,即传入什么直接替换,例如${column}=id和${column}='id'则会产生不同的效果。而此类语句有可能会产生sql的注入,经典的例子:select * from ${tableName} where name = #{name};传入变量tableName= "user;delete user;-- " 动态解析后的sql:select *fromuser;deleteuser;-- where name = ?;会存在注入。如果select * from #{tableName} where name = #{name};传入tableName="user" 则产生无法编译的sql错误。
结论:能使用#{}尽量使用,因为i#{}可以产生一个预编译对象,可以重复利用,不会产生sql注入,当语句必须使用表明或者字段名时,需要排除sql注入的可能,必须使用${}。