MyBatis -- 参数占位符 #{} 和 ${}
- 一、准备工作
- 二、参数占位符 #{} 和 ${}
- 三、特殊场景
- 3.1 特殊场景 1 -- String
- 3.1.1 使用 #{}
- 3.1.2 使用 ${}
- 3.1.3 分析与解决
- 3.2 特殊场景 2 -- MySQL 关键字
- 3.3 特殊场景 3 -- SQL 注入问题 (重要)
- 3.4 特殊场景 4 -- 模糊查询 like
一、准备工作
创建数据库和表
-- 创建数据库
drop database if exists mycnblog;
create database mycnblog DEFAULT CHARACTER SET utf8mb4;
-- 使用数据数据
use mycnblog;
-- 创建表[用户表]
drop table if exists userinfo;
create table userinfo(
id int primary key auto_increment,
username varchar(100) not null,
password varchar(32) not null,
photo varchar(500) default '',
createtime datetime default now(),
updatetime datetime default now(),
`state` int default 1
) default charset 'utf8mb4';
添加实体类
package com.example.demo.model;
import lombok.Data;
import java.util.Date;
/**
* 普通的实体类,用于 Mybatis 做数据库表(userinfo表)的映射
* 注意事项:保证类属性名称和userinfo表的字段完全一致!
*/
@Data
public class UserInfo {
private int id;
private String name;
private String password;
private String photo;
private Date createtime;
private Date updatetime;
private int state;
}
添加 mapper 接口 (数据持久层)
package com.example.demo.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper {
}
创建与接口对应的 xml 文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.mapper.UserMapper">
</mapper>
二、参数占位符 #{} 和 ${}
-
#{}:预编译处理。
预编译处理是指:MyBatis 在处理#{}时,会将 SQL 中的 #{} 替换为?号,使⽤ PreparedStatement 的 set ⽅法来赋值。 -
${}:字符直接替换。
直接替换:是MyBatis 在处理 ${} 时,就是把 ${} 替换成变量的值。
预编译处理和字符串替换的区别故事(头等舱和经济舱乘机分离的故事):
乘坐飞机,头等舱和经济舱的区别是很⼤的。
- ⼀般航空公司乘机都是头等舱和经济舱分离的,头等舱的⼈先登机,登机完之后,封闭经济舱,然后再让经济舱的乘客登机,这样的好处是可以避免浑⽔摸⻥,经济舱的⼈混到头等舱的情况,这就相当于预处理,可以解决程序中不安全(越权处理)的问题。
- ⽽直接替换的情况相当于,头等舱和经济舱不分离的情况,这样经济舱的乘客在通过安检之后可能越权摸到头等舱,如下图所示:
这就相当于参数直接替换,它的问题是可能会带来越权查询和操作数据等问题,⽐如后⾯会讲的 SQL 注⼊问题。
三、特殊场景
3.1 特殊场景 1 – String
3.1.1 使用 #{}
username 唯一。
插入一条数据:
-- 添加一个用户信息
INSERT INTO `mycnblog`.`userinfo` (`id`, `username`, `password`, `photo`, `createtime`, `updatetime`, `state`) VALUES
(1, 'admin', 'admin', '', '2021-12-06 17:10:48', '2021-12-06 17:10:48', 1);
UserMapper 接口:
// 根据用户姓名完全匹配查询
public UserInfo getUserByName(@Param("username") String username);
UserMapper.xml:
<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=#{username}
</select>
单元测试:
package com.example.demo.mapper;
import com.example.demo.model.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest // 当前测试的上下文环境为 springboot
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void getUserByName() {
UserInfo userInfo = userMapper.getUserByName("admin");
System.out.println("userinfo -> " + userInfo);
}
}
经测试,成功打印了我们插入的数据信息。
3.1.2 使用 ${}
UserMapper.xml:
<select id="getUserByName" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username=${username}
</select>
此时进行单元测试,报错了!:
3.1.3 分析与解决
- #{} 预编译处理:识别出来为字符串,就会自动加上引号
''
- ${} 字符直接替换:缺少引号,所以查找不到 username (去查找字段了!)
在其外加一层单引号:'${username}'
即可。
那么无论是 #{} 还是 ${},都加上
''
不就可以了吗?
理论上是可以的,但是会产生很多隐式类型转换!一旦存在隐式类型转换就不会走索引了,效率会大大降低!!!
3.2 特殊场景 2 – MySQL 关键字
例如排序时:order by xxx asc/desc
此时传递的虽然也是一个字符串,但是我们并不希望给它加上引号!
所以只能使用 ${} 字符直接替换!
UserMapper 接口:
// 查询所有的信息(根据排序条件进行排序)
public List<UserInfo> getAllByOrder(@Param("order") String order);
UserMapper.xml:
<select id="getAllByOrder" resultType="com.example.demo.model.UserInfo">
select * from userinfo order by id ${order}
</select>
但因为 SQL 注入问题,这种场景使用 ${} 时必须在 controller 中验证一下参数 (字符串必须是 asc/desc,否则代码就不要继续往下执行了!)
3.3 特殊场景 3 – SQL 注入问题 (重要)
<select id="isLogin" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username='${name}' and password='${pwd}'
</select>
sql 注入代码:' or 1='1
…
${} 字符直接替换,这时就忽略了密码直接登录了!十分危险!!!
因此绝大部分场景都会避免使用 ${}。
3.4 特殊场景 4 – 模糊查询 like
<select id="findUserByName2" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like '%#{username}%';
</select>
使用 #{} 会报错!
预编译会识别为字符串类型自动加上引号,不符合预期
相当于是:select * from userinfo where username like ‘%‘username’%’;
此时也并不能直接使用 ${}:会有 SQL 注入风险。在 controller 进行检查判断也不可取,因为传来的字符串是有无数种可能的,无法检查判断!
这时可以使用 MySQL 的内置函数 concat() 来处理,实现代码如下:
<select id="findUserByName3" resultType="com.example.demo.model.UserInfo">
select * from userinfo where username like concat('%',#{username},'%');
</select>
这种写法完全没有问题 ~