深入解析MyBatis中#{}和${}的区别与应用场景

在MyBatis框架的使用过程中,SQL映射文件的编写是核心工作之一。而#{}和${}这两种参数占位符语法,虽然看起来相似,却有着本质的区别。正确理解和使用它们,不仅关系到应用程序的安全性,还会影响系统性能。本文将全面剖析这两种语法的区别、实现原理、使用场景以及最佳实践。

一、基本概念与语法

1.1 #{}语法

#{}是MyBatis中的预编译占位符,也称为参数标记。它的基本形式如下:

复制代码
<select id="findUserById" resultType="User">
    SELECT * FROM users WHERE id = #{userId}
</select>

1.2 ${}语法

${}是MyBatis中的字符串替换占位符,也称为非转义字符串替换。它的基本形式如下:

复制代码
<select id="findUsersByTable" resultType="User">
    SELECT * FROM ${tableName} WHERE status = 1
</select>

二、底层实现原理

2.1 #{}的工作原理

当MyBatis遇到#{}时,会进行以下处理:

  1. 解析阶段:MyBatis解析SQL映射文件时,识别出#{}标记

  2. 参数处理:运行时将参数值通过PreparedStatement的set方法设置

  3. SQL生成:最终生成带有"?"的预编译SQL语句

例如上面的例子会生成:

复制代码
SELECT * FROM users WHERE id = ?

然后通过PreparedStatement的setInt/setString等方法设置参数值。

2.2 ${}的工作原理

对于${}的处理则完全不同:

  1. 直接替换:MyBatis在SQL解析阶段就直接将${}替换为实际的参数值

  2. 字符串拼接:替换后的SQL语句是通过字符串拼接而成的

  3. 直接执行:最终生成的是完整的SQL语句,而非预编译语句

例如,如果tableName="users",生成的SQL就是:

复制代码
SELECT * FROM users WHERE status = 1

三、核心区别对比

3.1 安全性对比

特性 #{} ${}
SQL注入防护 安全,能防止SQL注入 不安全,存在SQL注入风险
实现方式 参数化查询 字符串拼接

#{}示例:

复制代码
String sql = "SELECT * FROM users WHERE name = #{name}";
// 即使用户输入 name = "' OR '1'='1"
// 实际执行:SELECT * FROM users WHERE name = ? 
// 参数值会被正确处理,不会导致注入

${}示例:

复制代码
String sql = "SELECT * FROM users WHERE name = '${name}'";
// 如果用户输入 name = "' OR '1'='1"
// 实际执行:SELECT * FROM users WHERE name = '' OR '1'='1'
// 这将返回所有用户数据,造成SQL注入

3.2 性能对比

特性 #{} ${}
数据库优化 支持预编译,可缓存执行计划 每次都是新SQL,无法缓存
网络传输 只需传输参数 需要传输完整SQL
编译次数 一次编译多次执行 每次都需要重新编译

3.3 使用场景对比

场景 #{} ${} 说明
普通参数值 推荐使用#{}
表名 动态表名必须使用${}
列名 动态列名必须使用${}
ORDER BY子句 动态排序需谨慎使用
GROUP BY子句 动态分组需谨慎使用
LIKE模糊查询 需特殊处理通配符

四、深入应用场景

4.1 必须使用#{}的场景

  1. 所有用户输入的参数值

    复制代码
    WHERE username = #{username} AND password = #{password}
  2. 数值型参数

    复制代码
    WHERE age > #{minAge} AND age < #{maxAge}
  3. 日期型参数

    复制代码
    WHERE create_time > #{startDate}

4.2 可能需要使用${}的场景

  1. 动态表名

    复制代码
    SELECT * FROM ${tableName}

    适用于分表场景,如表名按年份分表:user_2022, user_2023等

  2. 动态列名

    复制代码
    SELECT ${columns} FROM users

    适用于动态选择返回字段的场景

  3. ORDER BY排序

    复制代码
    ORDER BY ${sortColumn} ${sortOrder}

    但更安全的做法是:

    复制代码
    <choose>
        <when test="sortColumn == 'name'">ORDER BY name</when>
        <when test="sortColumn == 'age'">ORDER BY age</when>
        <otherwise>ORDER BY id</otherwise>
    </choose>

4.3 特殊场景处理

LIKE模糊查询的正确写法:

错误方式:

复制代码
WHERE name LIKE '%${name}%'

正确方式:

复制代码
// Java代码中处理参数
String nameParam = "%" + name + "%";

WHERE name LIKE #{nameParam}

或使用SQL函数:

复制代码
WHERE name LIKE CONCAT('%', #{name}, '%')

五、最佳实践建议

5.1 安全性实践

  1. 默认使用#{}:除非确有必要,否则总是使用#{}

  2. **严格过滤{}参数**:使用{}时,必须对参数值进行白名单验证

    复制代码
    // 验证表名是否合法
    if (!isValidTableName(tableName)) {
        throw new IllegalArgumentException("Invalid table name");
    }
  3. 避免用户输入直接用于${}:特别是排序、分组等场景

5.2 性能优化实践

  1. 优先使用#{}:利用预编译语句的缓存优势

  2. **减少{}使用频率**:对于频繁调用的SQL,避免使用{}导致无法缓存执行计划

  3. 批量处理动态SQL:对于必须使用${}的场景,考虑批量处理减少SQL解析次数

5.3 代码可维护性实践

  1. 明确注释:在使用${}的地方添加注释说明原因

    复制代码
    <!-- 必须使用${}因为表名是动态的 -->
    SELECT * FROM ${tableName}
  2. 集中管理:将动态部分集中管理,便于维护和安全检查

  3. 单元测试:为使用${}的SQL编写额外的安全测试用例

六、常见问题解答

Q1:为什么ORDER BY不能使用#{}?

A:因为#{}会给参数值添加引号,例如:

复制代码
ORDER BY 'name' 'DESC'  -- 错误的SQL语法

而正确的应该是:

复制代码
ORDER BY name DESC

Q2:什么情况下必须使用${}?

A:当SQL语句的非参数部分需要动态变化时,如:

  • 动态表名

  • 动态列名

  • SQL关键字(如ASC/DESC)

Q3:如何安全地使用${}?

A:可以采取以下措施:

  1. 使用白名单验证参数值

  2. 避免直接使用用户输入

  3. 对参数值进行转义处理

  4. 最小化使用范围

总结

#{}和${}在MyBatis中扮演着不同的角色:

  • #{} 是安全的参数占位符,适用于几乎所有参数值的场景,能防止SQL注入,性能更好。

  • ${} 是字符串替换,适用于SQL语句本身需要动态变化的场景,但存在安全风险,应当谨慎使用。

在实际开发中,我们应该遵循以下原则:

  1. 默认使用#{}

  2. 谨慎评估${}的使用必要性

  3. 对必须使用${}的场景实施严格的安全控制

  4. 编写清晰的文档和注释说明使用原因

正确理解和使用这两种占位符,将使你的MyBatis应用更加安全、高效和可维护。