一、概述:
1. 数据类型匹配
#{} 会进行预编译,而且进行类型匹配;
${} 不进行数据类型匹配。
2. 实现方式
#{} 用于变量替换;
${} 实质上是字符串拼接。
3. #和$的使用场景
(1)变量的传递,必须使用#,使用 #{} 就等于使用了PrepareStatement这种占位符的形式,提高效率。可以防止sql注入等等问题。#{} 方式一般用于传入添加,修改的值或查询,删除的where条件id值,例如:
select * from t_user where name = #{param};
(2) ${}只是只是简单的字符串拼接,要特别小心sql注入问题,对应非变量部分,只能用$。${}方式一般用于传入数据库对象字段,比如这种group by 字段 ,order by 字段,表名,字段名等没法使用占位符的就需要使用${},例如:
select count(*), from t_user group by ${param}
(3)能同时使用#和$的时候,最好用#。
二、SQL注入
2.1 ${}方式是将形参和SQL语句直接拼接形成完整的SQL命令后,再进行编译。所以可以通过设计形参变量的值,从而改变SQL语句产生安全隐患,即为SQL注入攻击。举例说明:
在Mapper映射文件中:
<select id="findByName" parameterType="String" resultMap="ResultMap">
SELECT * FROM user WHERE username='${value}'
</select>
当username=" 'or 1=1 or' "传入后,${}将变量内容直接和sql语句进行拼接。
SELECT * FROM user WHERE username='' OR 1=1 OR '';
该跳语句可以将整个数据库用户内容暴露出来。
2.2 #{}方式则是先用占位符代替参数将SQL语句先进行编译,然后再讲参数中的内容替换进去。因为SQL语句已经被预编译过,其SQL意图将无法通过非法的参数内容实现更改,其参数中的内容,仅仅是待填充的值而已,无法变为SQL命令的一部分。故,#{}可以防止SQL注入而${}却不行
情况一:只用 #{}
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select * from USER where userName=#{userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select * from USER where userName=? and userPassword =?; ==> Parameters: bob(String), 123123(String) <== Total: 1
结论:
#{} 会在预编译期,生成两个 ? ,作为占位符。
情况二:一个用 ${} 一个用 #{}
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select * from USER where userName=${userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select * from USER where userName=bob and userPassword =?; ==> Parameters: 123123(String)
结论:很显然 ${} 是直接拼成字符串的 ,#{} 是生成 ?占位符的。
而且因为 userName:bob 是字符串,所以 这种写法显然也是错误的。
会报出如下错误:
Error querying database. Cause: java.sql.SQLSyntaxErrorException: Unknown column 'mww' in 'where clause'
所以正确的写法是这样的:为字符串字段加上单引号 ' '
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select * from USER where userName='${userName}' and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select * from USER where userName='mww' and userPassword =?; ==> Parameters: 123(String) <== Total: 1
结论:显然这种写法是正确的,从这里可以看出,预编译期 ${} 是直接把参数拼接到SQL中,运行时,就只传入了一个 #{} 修饰的参数。${}的这种写法还有一个安全隐患,那就是 SQL注入。
情况三:${} SQL注入:
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select * from USER where userName='${userName}' and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select * from USER where userName='' OR 1=1 OR '' and userPassword =?;
==> Parameters: 123123(String) <== Total: 2
结论:只要我们在 ${} 输入 ' OR 1=1 OR ' 无论后面的密码输入什么都可以,查询到数据,这种情况就是SQL注入。
情况四:#{} 防止SQL注入
<select id="getUserByNameAndPsw" parameterType="map" resultType="com.hotel3.model.User">
select * from USER where userName=#{userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select * from USER where userName=? and userPassword =?;
==> Parameters: ' OR 1=1 OR '(String), 123123(String) <== Total: 0
结论:上面预编译SQL的参数已经由占位符 { ?} 代替,所以传入的 ' OR 1=1 OR ' 只会作为 userName字段的值,而不会拼入执行的SQL。这样就达到了防止SQL注入的目的。
在这种用法中, #{} 显然比 ${} 用法更好。那 ${} 为什么经常在 Mybatis 使用那?
${}正确用法(个人见解):
1. 同时传入一个字段名和字段值:
User u=userService.getUserByNameAndPsw("userName,userType,userPassword",userName,userPassword);
SQL: select ${arg0} from USER
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select ${arg0} from USER where userName=#{userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select userName,userType,userPassword from USER where userName=? and userPassword =?;
==> Parameters: bob(String), 123123(String) <== Total: 1
结论:
生成了我们想要SQL语句 :select userName,userType,userPassword from USER。
2、传入两个字段名和字段值:
User u=userService.getUserByNameAndPsw("userName,userType,userPassword",userName,userName,userPassword);
SQL: select ${arg0} from USER where ${arg1}=#{userName}
<select id="getUserByNameAndPsw" resultType="com.hotel3.model.User">
select ${arg0} from USER where ${arg1}=#{userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select userName,userType,userPassword from USER where userName=? and userPassword =?;
==> Parameters: bob(String), 123123(String) <== Total: 1
结论:
按照传参的顺序匹配 SQL 中的 ${arg0},${arg1},生成我们想要的代码,但这个方式会使 Mybatis 的 Mapper 文件可读性变差,如果不看其他的代码,很难辨别 ${arg0} ,${arg1} 代表的含义。
3、使用Map传值,提高 Mapper 文件的可读性
Map map =new HashMap();
map.put("selectValues","userName,userType,userPassword");
map.put("userNamefieId","userName"); map.put("userName",userName);
map.put("userPassword",userPassword);
User u=userService.getUserByNameAndPsw(map);
Mapper 文件的 xml
<select id="getUserByNameAndPsw" parameterType="map" resultType="com.hotel3.model.User">
select ${selectValues} from USER where ${userNamefieId}=#{userName} and userPassword =#{userPassword};
</select>
结果:
==> Preparing: select userName,userType,userPassword from USER where userName=? and userPassword =?;
==> Parameters: bob(String), 123123(String) <== Total: 1
结论: 结果和arg0、arg1的传值方式相同,但 Mapper 文件的 xml 中的SQL语句可以。
通过对 map 中 key 值命名提高可读性。
注1:以上SQL,均为预编期生成的SQL。
注2:${} 每次传值不同 SQL 语句不同,可以灵活生成 SQL,但每次都需要进行预编译,对效率会有影响,至于要不要使用 ${}还要具体情况具体分析。
三、顺带提一嘴预编译
1. 三个阶段:
- 词法和语义解析
- 优化sql语句,制定执行计划
- 执行并返回结果
2. 预编译出现的原因
- 很多情况下,一条SQL语句可能会反复执行,或者
每次执行的时候只有个别的值不同
; - 比如query的where条件的值不同,update的set的值不同,insert的values值不同,都会造成SQL语句的不同。
- 每次因为这些值的不同就进行词法语义解析、优化、制定执行计划,就会很影响效率。
- 而且往往 步骤 1、2 加起来的时间比 步骤 3的时间还要长。
这种情况下就需要预编译的出场了。
3. 预编译
3.1 预编译:
指的是数据库驱动在发送 sql 语句和参数给 DBMS 之前对 sql 语句进行编译,这样 DBMS 执行 sql 时,就不需要重新编译。
3.2 预编译的好处:
- 预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译。
- 越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。
- 相同的预编译 SQL 可以重复利用。(把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个 SQL,可以直接使用这个缓存的 PreparedState 对象。)
- 可以将这类SQL语句中的值用占位符替代,不需要每次编译,可以直接执行,只需执行的时候,直接将每次请求的不同的值设置到占位符的位置。
- 预编译可以视为将sql语句模板化或者说参数化。