前言: 本文将简单介绍一下子啊Mybatis中对sql语句中的参数传入的字段匹配的两个匹配方式 #{} 和${} ,这两种方式的区别
在使用mybatis对数据库进行操作时,我们通常不能把sql语句写死,而是在测试用例中可以通过设置参数来动态调整执行的sql ,这就需要把传入的字段 ,作为参数传入给sql 中进行执行, 如何实现这一点呢? 这就需要#{} 和${}这两种方式了
先给出结论,上面的两个方式都可以完成把输入的参数完成字段和sql的匹配,那么这两个之间有什么区别? 在实际使用中又更推荐哪一种方式呢? 下面来一一道来
一. 语义本质:参数占位 vs. 文本替换
-
#{} (Parameter Placeholder) :映射为 JDBC 的
PreparedStatement。采用预编译 (Pre-compilation) 机制,将 SQL 结构与数据分离。 -
${} (String Substitution) :映射为 JDBC 的
Statement。执行静态字符串拼接,SQL 结构随参数动态改变。
简单来说, #{ } 是 参数占位符,而 ${ }是进行字符串拼接的方式来完成的
比如我们熟知的打印Print()方法, 如果需要把变量a 和b 的值进行打印输入,中间还包含其他字符串的话,对于简单的输出语句,我们可以直接使用字符串拼接的方式来实现, 对于复杂的语句使用占位符进行匹配输入
java
//1.字符串拼接
System.out.println("a:" + a + " , b:" + b);
//2.占位符匹配
System.out.printf("a:%d , b:%d",a,b);
下面的则是使用xml对sql进行占位符字段匹配的代码
java
<select id="selectById" resultType="java.lang.Integer">
select id from user_info where id = #{id}
</select>
#{} 就体现了这种占位符的解耦特性:先定义好一句话的结构,告诉它该填入什么类型的数据即可.
但是#{} 和printf() 的本质就都只是简单的占位吗?实则不然
1.#{}的本质
虽然 printf 看起来像占位符,但它与 MyBatis 的 #{} 在 数据库层面 有一个决定性的不同:
-
Java 的
printf: 最终还是在你的 CPU 里把字符串拼好了,再输出。 -
MyBatis 的
#{}(PreparedStatement): * 它不只是在 Java 里占位。-
它会把
SELECT * FROM users WHERE id = ?这条带问号的指令先发给数据库(MySQL)。 -
MySQL 会先"检查并锁定"这个 SQL 的执行计划(比如决定走哪个索引)。
-
最后你再把参数
1发过去,MySQL 直接填坑执行。 -
核心结论: 即使参数里有破坏性的指令,由于数据库已经预先定死了 SQL 的"骨架",那些破坏性指令也只能被当成普通的"废话字符串"。而不会对"骨架做出影响"
-

2. #{}的使用
下面是一个使用#{}进行参数占位的sql语句
先在mapper的目录下的对应接口中声明方法
java
@Mapper
public interface UserInfoMapperXML {
//按照id进行查询
UserInfo queryById(Integer id);
}
随后有两种方式来实现这个sql方法,这里我使用了在配置的xml文件中去实现这个sql,并把传入的id作为参数来进行sql查询
java
<?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.amadeus.mybatisdemo.mapper.UserInfoMapperXML">
//按照id查询
<select id="queryById" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where id = #{id}
</select>
</mapper>
随后在test文件中编写单元测试,一键运行
java
//SpringBootTest: 提供测试环境,启动Spring上下文
@SpringBootTest
class UserInfoMapperXMLTest {
//把容器中的接口Bean注入到测试类中
@Autowired
private UserInfoMapperXML userInfoMapperXML;
@Test
void queryById() {
UserInfo userInfo = userInfoMapperXML.queryById(3);
System.out.println(userInfo);
}
}

和我的数据库中存储信息对比,观察到确实可以查询出id = 3 的数据信息

这种运行sql中包含 ? 的sql ,属于是预编译sql
预编译的核心机制:占位符
预编译的核心在于将 SQL 模板 与 实际参数 分离。SQL 语句中使用占位符(如 JDBC 中的 ?)代替具体数值。
-
非预编译: 每次执行都要重新编译 SQL,解析字符串。
-
预编译: SQL 引擎仅编译一次模板。后续执行只需注入参数,无需重新解析 SQL 逻辑。
2.1 为什么需要预编译?
-
性能优化: 对于高频重复执行的 SQL,省去了重复编译的时间开销。
-
安全性(核心): 彻底杜绝 SQL 注入。参数被视为单纯的"字面量",不会被解释为 SQL 命令。
3. ${}的使用
把上面的代码中的#{} 修改为${} ,观察是否可以查询出对应的信息
修改XML文件中的sql代码即可
java
<select id="queryById" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where id = ${id}
</select>

3.1参数为String的使用差异
如果传入的参数是一个String类型的,这里我定义一个sql,是按照username进行查询
java
<select id="queryByUsername" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username = #{username}
</select>
如果使用#{} ,此时可以正常查询出结果

如果修改#{} 为${}呢? 运行测试方法,观察结果

那么差异显而易见: 使用${} 传入String类型的参数 时,会出现格式问题 ,传入的String参数没有" " 包裹
解决方法: 添加 " " 修订格式
使用${}的同时,添加" "
java
<select id="queryByUsername" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username = "${username}"
</select>
运行单元测试 , 查询成功

既然#{} 和${} 都可以完成把传入的字段值作为sql中的查询的条件字段 , 并都能完成查询任务, 那这是否代表着 我们在实际使用中 使用这两个方式都可以呢? 这两个方式之间是否存在差异和优劣?
答案自然是有的
先给出结论:
使用${} 会存在sql注入的隐患
二, ${} 使用隐患: SQL注入
${} 使用字符串拼接的方式来把传入的参数作为sql 命令和原sql模版进行拼接, 从而解析整条sql ,这一操作自然存在SQL注入的风险,
比如,这是一条按照username进行查询的sql指令
java
<select id="queryByUsername" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username = "${username}"
</select>
比如,如果用户不按常理输入,传入参数为 " or "1"="1 , 会发生什么?

${} 使用存在SQL注入的风险,此时若是使用 #{} 则会查询失败. 保护了数据库
所以在实际使用中,我们更推荐使用#{} 这种更安全的写法
可以看到,{} 存在安全隐患,在参数为String 时还需要手动修改sql , 参数外层添加"" ,这是否代表着 #{ } 可以完全取代 { } 呢? 实则不然, ${ } 还是存在一定的使用场景的
三,什么时候必须用 ${} ?
1. 对结果集排序
有时候我们需要对查询出的结果集,按照某一字段值,来进行升序 或者降序排序时 ,就需要使用order by
由于排序sql 中order by ,中 的排序规则, desc 和asc , 在sql中是不需要添加 " " 的,而是一个关键字, 所以我们如果使用#{ } 传入String 类型的 asc 和desc 时,会查询失败 ,如下:

但是如果使用{ } 就可以很好的完成这个任务, 因为 使用 { } 时我们可以手动选择不添加" " ,让mysql 把 desc 识别为关键字
java
<select id="queryAllSort" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info order by id ${sort}
</select>

2. like查询
有时我们需要对查询的结果集中做筛选, 使用like 筛选包含特定字符的行数据
这里我们来查询结果中 username 中包含 ee 这个字符的数据
java
List<UserInfo> queryAllUserByLike(String key);
<select id="queryAllUserByLike" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username like '%#{key}%'
</select>
@Test
void queryAllUserByLike() {
System.out.println(userInfoMapperXML.queryAllUserByLike("ee"));
}
运行,测试,执行失败

此时使用${ } 虽然存在SQL注入的风险,但还是可以解决这个问题,
java
<select id="queryAllUserByLike" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username like '%${key}%'
</select>

不过还可以使用concat 来结合 #{ } 实现 模糊查询
CONCAT(str1, str2, ...) 接受一个或多个参数,并返回拼接后的字符串。
java
<select id="queryAllUserByLike" resultType="com.amadeus.mybatisdemo.model.UserInfo">
select * from user_info where username like concat('%', #{key}, '%')
</select>

四, 总结与开发建议
-
优先级原则 :首选 #{},这是开发规范中的强制要求。
-
性能考量:频繁变动的 SQL 建议使用预编译,提升数据库执行计划的重用率。
-
初学者避坑指南:
-
传递
String类型参数时,#{}会自动加上单引号' ',而${}不会。 -
在 MyBatis 3.x 以后,若只有单个基本类型参数,
${}建议配合@Param注解使用。
-
有关mybatis中的#{ } 和${ }相关 介绍就到这里了,如有纰漏还请指出~~