mybatis中#{}和${}的区别?
#{}的原理
在MyBatis中,#{}
属于预编译的参数,它会将传入的参数视为一个占位符,并将其转化为一个安全可控的sql语句。在执行sql之前,预先设置好了sql语句中的参数,再将参数传入sql语句中进行执行。
例如
@Select("SELECT * FROM user WHERE username = #{username} AND password=#{password}")
User getUser(@Param("username") String username, @Param("password") String password);
${}的原理
${}属于字符串替换,它会将传入的参数直接拼接在sql语句中。因为它不进行预编译,所以存在SQL注入的风险。
例如
@Select("SELECT * FROM user WHERE username = '${username}' AND password=${password}")
User getUser(@Param("username") String username, @Param("password") String password);
这就会有sql注入的风险。
什么时候用${}
虽然${}
有sql注入的危险,但还是有些情况需要我们使用${}
。
当我们需要在SQL语句中传入表名或列名时,我们可以使用${},因为这个时候参数会作为一个字符串被拼接在sql语句中,并且这个参数是没有进行预编译的。如果我们使用#{},则会将传入的参数加入单引号。如果在SQL语句中传入字段名称或表名称,那么单引号就会产生问题。
例如
<insert id="insertData" parameterType="com.example.entity.Student">
INSERT INTO
${tableName}
(id,name,age)
VALUES
(#{id},#{name},#{age})
</insert>
${tableName}
是列名,如果你用#{}
就会自动加上单引号,这样是不行的,只能用${}
。
#{}和${}的使用区别
#{}的原理是在MyBatis源码中的处理过程中,将占位符替换成JDBC预编译语句中的“?”。例如在XML mapper文件中的SQL语句:
<select id="selectUserById" parameterType="int" resultType="com.example.User">
SELECT * FROM users WHERE id = #{id}
</select>
在实际执行时,MyBatis会将这个 SQL 中的 #{id} 替换成 ?,同时还会为预编译语句中的 ? 设置参数值。
而加上单引号这个做法则是因为如果不加单引号,一些类型的参数(比如字符串、日期等)在拼接SQL语句时会产生语法错误,因此MyBatis会自动在传入参数时加上单引号以避免这种错误。
例如,下面这个语句中的id参数可能是一个字符串类型:
<select id="selectUserById" parameterType="String" resultType="com.example.User">
SELECT * FROM users WHERE id = #{id}
</select>
在实际执行时,如果id参数的值是字符串类型的,则拼接出来的SQL语句就变成了:
SELECT * FROM users WHERE id = 'myExampleId'
如果没有单引号,就会变成:
SELECT * FROM users WHERE id = myExampleId
这段SQL语句会产生语法错误。
因此,MyBatis会自动在参数上加上单引号来避免这种错误。但也应该注意,不是所有的参数都需要加上单引号,比如数字类型的参数就不需要单引号。
#{}的源码实现
在 MyBatis 中,${}将参数直接拼接到 SQL 字符串中,而 #{}使用 PreparedStatement 的参数设置方式来实现。即在参数值传输到数据库驱动之前,Mybatis 的 SQL 解析引擎会将 SQL 中的 #{} 占位符替换为 ? ,然后调用 PreparedStatement 的 setXXX() 方法将实际的参数值传递到 SQL 中。从而避免 SQL 注入攻击的问题。
下面是 MyBatis#{}的实现过程,从执行器开始分析:
public class SimpleExecutor extends BaseExecutor {
// 重载了父类的 query 非根方法
@Override
protected <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
// 获得连接 & 创建 Statement 对象
stmt = createStatement(ms, parameter);
// 执行 Statement ,返回结果集 ResultSet
ResultSet rs = stmt.executeQuery(boundSql.getSql());
// 处理 ResultSet ,封装成 ResultHandler 等
return (List<E>) resultHandler.handleResultSets(rs);
} finally {
// 关闭 Statement 对象
closeStatement(stmt);
}
}
private Statement createStatement(MappedStatement ms, Object parameter) throws SQLException {
Configuration configuration = ms.getConfiguration();
// 获得 DataSource
Environment environment = configuration.getEnvironment();
DataSource dataSource = environment.getDataSource();
// 创建 Connection 对象
Connection connection = dataSource.getConnection();
// 创建 StatementHandler 对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建 Statement 对象
Statement stmt = null;
// 准备 Statement 对象
stmt = handler.prepare(connection, transaction.getTimeout());
// 设置参数到 Statement 对象中
handler.parameterize(stmt);
return stmt;
}
// ...
}
可以看到,MyBatis会依次经过如下几个阶段执行:
1.创建 Statement 对象;
2.完成 Statement 对象的参数设置;
3.执行 Statement ,得到结果集 ResultSet ;
4.ResultHandler 处理结果集 ResultSet ,得到结果 。其中,第2步完整的调用栈为:
public class MybatisParameterHandler implements ParameterHandler {
// 重要代码
@Override
public void setParameters(PreparedStatement ps) throws SQLException {
ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings != null) {
// 遍历 ParameterMapping 数组,并设置参数到 SQL 中
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
TypeHandler<?> typeHandler = parameterMapping.getTypeHandler();
JdbcType jdbcType = parameterMapping.getJdbcType();
if (value == null && jdbcType == null) {
jdbcType = configuration.getJdbcTypeForNull();
}
typeHandler.setParameter(ps, i + 1, value, jdbcType);
}
}
}
}
}
MyBatis会先遍历 ParameterMapping 数组,并设置参数到 SQL 中。在这个过程中,MyBatis 会先为 setObject() 方法传递参数的索引,然后根据 Java 类型,调用对应的 TypeHandler 完成参数设置。在这个过程中,MyBatis 还会判断: 当前的参数值是否为 null(null 要么用默认类型,要么用 parameterMapping 中指定的类型)、JDBC 类型是否为 null,并且还需要考虑 JDBC 类型和 Java 类型的映射问题。最终,MyBatis 会将参数设置出去,等待 PreparedStatement 对象执行 SQL语句。
总结
#{}进行预编译,${}进行字符串替换
#{}可避免SQL注入,${}存在SQL注入风险
当传入参数时,使用#{},当传入表名或列名时,使用${}。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/196228.html