[Mybatis] #{ } 与 ${ } 的底层博弈与工程实践

前言: 本文将简单介绍一下子啊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 的 #{}数据库层面 有一个决定性的不同:

  1. Java 的 printf 最终还是在你的 CPU 里把字符串拼好了,再输出。

  2. 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 为什么需要预编译?

  1. 性能优化: 对于高频重复执行的 SQL,省去了重复编译的时间开销。

  2. 安全性(核心): 彻底杜绝 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中的#{ } 和${ }相关 介绍就到这里了,如有纰漏还请指出~~

相关推荐
2601_9498177217 小时前
Spring Boot3.3.X整合Mybatis-Plus
spring boot·后端·mybatis
LaLaLa_OvO1 天前
mybatis 引用静态常量
java·mybatis
yaodong5181 天前
Spring 中使用Mybatis,超详细
spring·tomcat·mybatis
2601_949815331 天前
Spring Boot中集成MyBatis操作数据库详细教程
数据库·spring boot·mybatis
星晨雪海1 天前
若依框架原有页面功能进行了点位管理改造之列表查询(4)
数据库·sql·mybatis
mldlds1 天前
Spring Boot 集成 MyBatis 全面讲解
spring boot·后端·mybatis
那个失眠的夜1 天前
Spring整合Mybatis实现用户的CRUD
java·spring·mybatis
zjjsctcdl1 天前
Spring Boot 整合 MyBatis 与 PostgreSQL 实战指南
spring boot·postgresql·mybatis
zmsofts2 天前
java面试必问13:MyBatis 一级缓存、二级缓存:从原理到脏数据,一篇讲透
java·面试·mybatis