基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

MyBatis:#{}与${}区别

知识点图片

#{}是预编译参数,安全防注入,用于传递值。${}是字符串替换,有SQL注入风险,用于动态传入表名、列名等SQL关键字。

这是一个MyBatis中非常核心且重要的问题。简单来说,#{}${} 的主要区别在于安全性、处理方式和使用场景

下面我将从这几个方面详细解释它们的区别。


核心总结

特性 #{} (预编译参数) ${} (字符串替换)
工作机制 使用JDBC的 PreparedStatement 简单的字符串拼接/替换
安全性 安全,能有效防止SQL注入 不安全,有SQL注入风险
类型处理 自动进行数据类型转换 不进行类型处理,直接是文本替换
性能 数据库可预编译和缓存执行计划,性能稍高 每次都是新的SQL,数据库需要重新解析
使用场景 传递参数值,如 WHERE 条件、UPDATESET 值等 动态指定SQL关键字或标识符,如表名、列名、ORDER BY

1. #{} (预编译参数 - Parameter Placeholder)

#{} 是MyBatis中最常用、也是最推荐的方式。

工作原理

它在处理时,会将SQL中的 #{} 替换为 ? 占位符,然后通过JDBC的 PreparedStatement 来设置参数。

示例:

  • Mapper XML:

    xml
    <select id="getUserById" resultType="com.example.User">
        SELECT * FROM users WHERE id = #{userId}
    </select>
  • 当传入 userId = 10 时,MyBatis执行的流程如下:

    1. 预编译SQL: 将SQL语句发送到数据库进行预编译:
      sql
      SELECT * FROM users WHERE id = ?
    2. 设置参数: 调用 PreparedStatementset 方法(如 setInt, setString)来设置参数 ? 的值为 10

优点

  1. 防止SQL注入 (最重要!)
    因为参数值是作为独立的数据传递给数据库的,而不是作为SQL语句的一部分。数据库不会将参数值解释为SQL命令。
    例如,如果一个恶意用户传入 userId = "10 OR 1=1",最终设置到 ? 里的值就是字符串 "10 OR 1=1",数据库会把它当成一个普通的字符串去匹配 id,而不会执行 OR 1=1 这个逻辑。

  2. 类型安全
    MyBatis会根据参数的Java类型自动转换为对应的JDBC类型,无需手动处理。

  3. 性能
    数据库可以对预编译的SQL语句进行缓存和优化,当相同的SQL语句(只是参数不同)再次执行时,可以重用执行计划,提高性能。


2. ${} (字符串替换 - String Substitution)

${} 的工作方式非常直接:它就是简单的字符串替换。

工作原理

在SQL语句发送到数据库之前,MyBatis会用 ${} 中的变量值直接替换掉 ${} 本身,形成一个完整的SQL字符串。

示例:

  • Mapper XML:

    xml
    <select id="getUsersByOrder" resultType="com.example.User">
        SELECT * FROM users ORDER BY ${columnName}
    </select>
  • 当传入 columnName = "create_time" 时,MyBatis生成的最终SQL是:

    sql
    SELECT * FROM users ORDER BY create_time

    这个完整的SQL字符串会被发送到数据库执行。

缺点和风险

  1. SQL注入风险 (非常危险!)
    这是 ${} 最大的问题。因为它只是简单的字符串拼接,如果传入的值来自用户输入,而没有经过严格的校验,就会导致SQL注入。

    SQL注入攻击示例:
    假设有这样一个查询:

    xml
    <select id="getUserByName" resultType="com.example.User">
        SELECT * FROM users WHERE name = '${userName}'
    </select>

    如果一个恶意用户传入 userName 的值为:' or '1'='1
    那么拼接后的SQL将变为:

    sql
    SELECT * FROM users WHERE name = '' or '1'='1'

    这个SQL的 WHERE 条件永远为真,将返回 users 表中的所有数据,造成数据泄露。

合理的使用场景

尽管 ${} 很危险,但在某些特定场景下,它又是必须的,因为 PreparedStatement 的参数占位符 ? 只能用来替换,不能用来替换SQL的结构性部分,如:

  1. 动态指定表名或库名

    xml
    <select id="getDataFromTable" resultType="map">
        SELECT * FROM ${tableName}
    </select>
  2. 动态指定排序列 (ORDER BY)

    xml
    <select id="getUsers" resultType="com.example.User">
        SELECT * FROM users ORDER BY ${orderByColumn} ${sortDirection}
    </select>
    <!-- 调用时:getUsers("id", "DESC") -->
  3. 动态指定要查询的列

    xml
    <select id="getColumns" resultType="map">
        SELECT ${columnList} FROM users
    </select>
    <!-- 调用时:getColumns("id, name, email") -->

使用 ${} 的安全须知:
当必须使用 ${} 时,永远不要直接使用前端或用户传入的原始值。你必须在后端对这些值进行严格的白名单校验,确保它们是你期望的、安全的标识符(如表名、列名)。


总结与最佳实践

  1. 首选 #{}:在任何可以传递参数值的地方(如 WHERE 条件、INSERTVALUESUPDATESET),都应该强制使用 #{}。这是MyBatis开发的黄金法则。

  2. 慎用 ${}:只在需要动态改变SQL结构(如表名、列名)时才使用 ${}

  3. 校验 ${} 的输入:当使用 ${} 时,传入的参数必须来自后端代码的控制,或者对用户输入进行了严格的白名单验证,确保其不是恶意的SQL代码片段。

记住这个简单的口诀:“能用 # 就用 #$ 只用在SQL关键字上,且必须校验”。

00:00
00:00