1. ${ } 和 #{ }
MyBatis获取参数值的两种方式:${ } 和 #{ }
对于初学者来说,理解MyBatis中获取参数值的两种方式——#{}
和${}
,关键在于明白它们如何影响SQL语句的构建以及为何在安全性、灵活性上有显著差异。下面我将用简单易懂的语言来解释这两者的本质、工作原理及使用注意事项。
1. ${}
:字符串拼接
本质:${}
在MyBatis中被视为字符串替换符。当你在SQL语句中使用${}
时,MyBatis会直接将它包围的变量名替换为实际传入的值。这种方式类似于在编程语言中进行字符串拼接操作,即将传入的值作为文本片段插入到SQL语句中。
工作原理:
SELECT * FROM users WHERE username = '${username}'
假设传入的username
参数为 'zhangsan'
,MyBatis会将上述SQL语句中的${username}
替换为 'zhangsan'
,生成如下已拼接好的SQL:
SELECT * FROM users WHERE username = 'zhangsan'
特点与注意事项:
- 手动处理引号:由于
${}
直接参与字符串拼接,因此对于字符串类型或日期类型的字段值,你需要确保传入的值已经正确地加上了单引号。例如,如果你传入的是'zhangsan'
而不是zhangsan
,MyBatis不会帮你自动加上单引号。 - SQL注入风险:由于
${}
直接将传入的值作为原始文本插入到SQL语句中,没有进行任何预处理或转义,因此它不提供对SQL注入攻击的防护。如果传入的值来自不可信的用户输入,恶意用户可能通过构造特定的输入来篡改SQL语句的结构和意图,从而对数据库造成潜在威胁。因此,除非有明确需求且能够确保输入安全,否则应避免在条件查询等涉及用户输入的地方使用${}
。 - 适用场景:
${}
通常用于那些需要原样插入到SQL语句中且不会引起SQL注入风险的情况,如动态表名、列名(虽然不推荐),或者在你已经确保传入值安全的情况下。
2. #{}
:预编译占位符
本质:#{}
在MyBatis中是一个预编译占位符。当SQL语句中包含#{}
时,MyBatis会将其替换为一个问号(?
),并在执行SQL时使用PreparedStatement来设置参数值。这种方式利用数据库的预编译机制,确保了参数值的安全性和类型正确性。
工作原理:
SELECT * FROM users WHERE username = #{username}
同样假设传入的username
参数为 'zhangsan'
,MyBatis会将上述SQL语句中的#{username}
替换为 ?
,然后使用PreparedStatement向数据库发送如下预编译SQL:
SELECT * FROM users WHERE username = ?
同时,MyBatis会将 'zhangsan'
作为参数值,通过PreparedStatement的set方法安全地绑定到SQL语句中的对应位置。
特点与注意事项:
- 自动处理引号:使用
#{}
时,MyBatis会根据参数的实际类型自动为其添加合适的引号或进行其他必要的类型转换。对于字符串和日期类型,MyBatis会自动加上单引号。所以你不需要担心引号问题,只需传入值即可。 - 防SQL注入:
#{}
利用预编译机制,确保参数值与SQL语句主体分离,由数据库在执行时负责正确的参数绑定。这从根本上杜绝了SQL注入攻击的可能性,极大地增强了系统的安全性。 - 适用场景:
#{}
是推荐的参数传递方式,适用于所有需要动态传入值的场景,特别是涉及到用户输入或敏感信息的条件查询、更新操作等。
举个代码例子:
1. ${}
:字符串拼接
想象一下,我们有一个简单的executeQuery()
方法,它接受一个用户输入的字符串(比如用户名),然后拼接到SQL查询语句中。
public List<User> executeQuery(String unsafeUsername) {
// 直接拼接字符串,模拟${}的行为
String sql = "SELECT * FROM users WHERE username = '" + unsafeUsername + "'";
// 假设这里有代码执行SQL查询并返回结果...
// executeSQL(sql);
return null; // 这里仅作演示,实际应返回查询结果
}
现在,假设用户输入了一个恶意的用户名,如:
String maliciousUsername = "' OR '1'='1"; // 模拟SQL注入攻击
调用executeQuery(maliciousUsername)
后,生成的SQL语句将是:
SELECT * FROM users WHERE username = '' OR '1'='1'
由于直接拼接字符串,恶意用户成功篡改了SQL语句的结构,导致查询结果不受控制,暴露了SQL注入风险。
2. #{}
:预编译占位符
接下来,我们用类似的方式模拟#{}
的工作原理,使用PreparedStatement来设置参数值。
public List<User> executePreparedQuery(String safeUsername) {
String sql = "SELECT * FROM users WHERE username = ?"; // 使用占位符
try (Connection conn = getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, safeUsername); // 安全地设置参数值
// 假设这里有代码执行PreparedStatement并返回结果...
// ResultSet rs = pstmt.executeQuery();
// return processResultSet(rs); // 这里仅作演示,实际应处理结果集并返回结果
} catch (SQLException e) {
throw new RuntimeException(e);
}
return null; // 这里仅作演示,实际应返回查询结果
}
同样,假设用户输入了恶意的用户名:
String maliciousUsername = "' OR '1'='1";
调用executePreparedQuery(maliciousUsername)
时,尽管恶意用户名被传递给了PreparedStatement,但由于预编译机制,数据库会将它视为一个独立的字符串值,而不是SQL语句的一部分。生成并执行的SQL实际上等同于:
SELECT * FROM users WHERE username = ''' OR ''1''=''1''
可以看到,尽管用户输入了恶意内容,但预编译机制确保了参数值被正确地转义和隔离,有效地防止了SQL注入攻击。
小总结
${}
就像在Java代码中直接拼接字符串,将用户输入的值原样插入到SQL语句中,需要手动处理引号,并且存在SQL注入风险。仅在确保输入安全且有特殊需求时考虑使用。#{}
则是通过预编译机制(如Java中的PreparedStatement),将参数值与SQL语句主体分离,由数据库在执行时负责正确的参数绑定。这样既自动处理了引号和类型转换,又从根本上杜绝了SQL注入攻击,是所有常规参数传递场景的推荐选择。
2. 单个字面量类型参数
当MyBatis的mapper接口中的方法参数仅为一个字面量类型(如Integer、String、Date等基本类型或其包装类),并且您需要在对应的SQL映射文件中引用这个参数时,可以使用${}
或#{}
来获取该参数的值。
考虑到使用#{}
更加安全可靠,所以就用它举个代码例子
UserMapper.java接口
package com.sakurapaid.mybatis3.demo01.mapper;
import com.sakurapaid.mybatis3.demo01.bean.User;
import java.util.List;
public interface UserMapper {
// 1.添加用户
public int addUser(User user);
// 2.修改用户
public int updateUser(User user);
// 3.查询所有用户
public List<User> findAllUser();
// 4.根据id删除指定用户
public int deleteUserById(int id);
// 5. 根据用户名查询用户
public User findUserByName(String name);
}
UserMapper.xml映射文件
这里的parameterType不是全类名,是因为我在配置文件中做了取别名操作
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 定义mapper接口的命名空间 -->
<mapper namespace="com.sakurapaid.mybatis3.demo01.mapper.UserMapper">
<!--1.添加用户-->
<insert id="addUser" parameterType="User">
insert into user(name,age,sex) values(#{name},#{age},#{sex})
</insert>
<!--2.修改用户-->
<update id="updateUser" parameterType="User">
update user set name=#{name},age=#{age},sex=#{sex} where id=#{id}
</update>
<!--3.查询所有用户-->
<select id="findAllUser" resultType="User">
select * from user
</select>
<!--4.根据id删除指定用户-->
<delete id="deleteUserById" parameterType="int">
delete from user where id=#{id}
</delete>
<!--5.根据用户名查询用户-->
<select id="findUserByName" resultType="User">
select * from user where name=#{name}
</select>
</mapper>
测试输出
package com.sakurapaid.mybatis3.demo01.test;
import com.sakurapaid.mybatis3.demo01.bean.User;
import com.sakurapaid.mybatis3.demo01.mapper.UserMapper;
import com.sakurapaid.mybatis3.demo01.utils.SqlSessionUtils;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 用户测试类
*/
public class UserTest {
@Test
public void test() throws IOException {
// 从SqlSessionUtils工具类获取SqlSession对象
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
// 获取UserMapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 1.添加用户
/*User user1 = new User(0, "小明", 18, "男");
int i = userMapper.addUser(user1);
if (i > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}*/
// 2.修改用户
/*User user2 = new User(1, "萨达姆", 26, "男");
int i = userMapper.updateUser(user2);
if (i > 0) {
System.out.println("修改成功");
} else {
System.out.println("修改失败");
}*/
// 3.查询所有用户
/*List<User> users = userMapper.findAllUser();
if (!users.isEmpty()) {
for (User user : users) {
System.out.println(user);
}
} else {
System.out.println("没有数据");
}*/
// 4.根据id删除指定用户
/*int i = userMapper.deleteUserById(4);
if (i > 0) {
System.out.println("删除成功");
} else {
System.out.println("删除失败");
}*/
// 5.根据用户名查询用户
User user = userMapper.findUserByName("萨达姆");
if (user != null) {
System.out.println("查询成功");
System.out.print(user);
} else {
System.out.println("没有数据");
}
}
}
3. 多个字面量类型参数
当MyBatis的mapper接口中的方法参数有多个字面量类型(如Integer、String、Date等基本类型或其包装类),MyBatis会以特定方式组织这些参数,以便在SQL映射文件中引用它们。
参数组织方式
- 默认键名:MyBatis会自动将这些参数放入一个Map集合中。每个参数以其在方法参数列表中的位置作为键名,即
arg0
,arg1
,arg2
, ...。例如,对于方法getUserInfo(int id, String name)
,参数id
对应的键为arg0
,参数name
对应的键为arg1
。 - 自定义键名:如果在方法参数上使用
@Param("paramName")
注解,MyBatis会使用注解中指定的名称作为键。例如,@Param("userId") int id
和@Param("userName") String name
,则键分别为userId
和userName
。
引用参数
- 使用
${}
或#{}
:在SQL映射文件中,可以通过${argN}
或#{argN}
(或使用自定义键名的${paramName}
、#{paramName}
)来访问Map集合中对应的值。其中,N
表示参数在方法参数列表中的位置(从0开始计数),paramName
为使用@Param
注解指定的名称。 - 手动添加单引号:对于
${}
,与单个参数情况相同,对于字符串或日期类型的值,需要手动添加单引号。而对于#{}
,仍然无需手动添加任何引号,MyBatis会自动处理。
示例
假设mapper接口方法为:
public User getUserInfo(@Param("userId") int id, @Param("userName") String name);
对应的SQL映射文件片段:
SELECT * FROM users WHERE id = #{userId} AND username = '${userName}'
再举一个代码例子
现在我要传入两个参数,id和name,来查询对应的用户信息
UserMapper.java
package com.sakurapaid.mybatis3.demo01.mapper;
import com.sakurapaid.mybatis3.demo01.bean.User;
import java.util.List;
public interface UserMapper {
// 1.添加用户
public int addUser(User user);
// 2.修改用户
public int updateUser(User user);
// 3.查询所有用户
public List<User> findAllUser();
// 4.根据id删除指定用户
public int deleteUserById(int id);
// 5.根据用户名查询用户
public User findUserByName(String name);
// 6.输入id和姓名,查询用户
public User findUserByIdAndName(int id, String name);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 定义mapper接口的命名空间 -->
<mapper namespace="com.sakurapaid.mybatis3.demo01.mapper.UserMapper">
<!--1.添加用户-->
<insert id="addUser" parameterType="User">
insert into user(name,age,sex) values(#{name},#{age},#{sex})
</insert>
<!--2.修改用户-->
<update id="updateUser" parameterType="User">
update user set name=#{name},age=#{age},sex=#{sex} where id=#{id}
</update>
<!--3.查询所有用户-->
<select id="findAllUser" resultType="User">
select * from user
</select>
<!--4.根据id删除指定用户-->
<delete id="deleteUserById" parameterType="int">
delete from user where id=#{id}
</delete>
<!--5.根据用户名查询用户-->
<select id="findUserByName" resultType="User">
select * from user where name=#{name}
</select>
<!--6.输入id和姓名,查询用户-->
<select id="findUserByIdAndName" resultType="User">
<!--select * from user where id=#{param1} and name=#{param2}-->
select * from user where id=#{arg0} and name=#{arg1}
</select>
</mapper>
如果此时sql语句还像单个字面量直接写标识符,编译就会报错
<!--6.输入id和姓名,查询用户-->
<select id="findUserByIdAndName" resultType="User">
<!--select * from user where id=#{param1} and name=#{param2}-->
<!--select * from user where id=#{arg0} and name=#{arg1}-->
select * from user where id=#{id} and name=#{name}
</select>
// 6.输入id和姓名,查询用户
User user = userMapper.findUserByIdAndName(1, "萨达姆");
if (user != null) {
System.out.println("查询成功");
System.out.print(user);
} else {
System.out.println("没有数据");
报错的关键简单总结就是,在执行MyBatis的数据库查询时,查询语句中引用了一个名为id的参数,但在对应的Mapper接口方法调用时并没有提供这个参数。MyBatis在处理SQL映射时未能找到与#{id}占位符相对应的参数值,因此抛出了org.apache.ibatis.binding.BindingException: Parameter 'id' not found的错误。这意味着在编写Mapper接口方法时,应当确保方法签名中包含所有在映射文件中使用的参数,同时在实际调用该方法时,也需要正确传递这些参数。
如何解决,此时就可以使用,也就是我注释上写的代码
默认键名:MyBatis会自动将这些参数放入一个Map集合中。每个参数以其在方法参数列表中的位置作为键名,即arg0
, arg1
, arg2
, ...。或者param1
, param2
, param3
, ...。
<!--6.输入id和姓名,查询用户-->
<select id="findUserByIdAndName" resultType="User">
<!--select * from user where id=#{param1} and name=#{param2}-->
<!--select * from user where id=#{arg0} and name=#{arg1}-->
</select>
还有有种方法是--自定义键名:如果在方法参数上使用@Param("paramName")
注解,MyBatis会使用注解中指定的名称作为键。例如,@Param("userId") int id
和@Param("userName") String name
,则键分别为userId
和userName
。
这个到文章的后面再讲
4. map集合类型的参数
若mapper接口中的方法需要的参数为多个时,此时可以手动创建map集合,将这些数据放在map中 只需要通过${}和#{}访问map集合的键就可以获取相对应的值,注意${ }需要手动加单引号
又举个代码例子
map集合方式输入id和姓名,查询用户
在 MyBatis 中,当一个 Mapper 接口方法需要接收多个参数时,可以使用 Map
集合作为参数类型,将各个参数以键值对的形式封装在 Map
中。这样,只需一个参数即可传递多个值,使得接口签名更为简洁。以下是具体的步骤和示例:
1. 定义 Mapper 接口方法
首先,在 UserMapper.java
中定义一个使用 Map
类型参数的方法,如:
public User findUserByIdAndName2(Map<String, Object> map);
此方法表示我们要根据 id
和 name
两个属性查询用户信息。
2. 编写 XML 映射文件
接着,在 UserMapper.xml
中编写对应的 SQL 查询语句,使用 #{}
占位符来引用 Map
中的键值:
<!-- 7.map集合方式输入id和姓名,查询用户 -->
<select id="findUserByIdAndName2" resultType="User">
select * from user where id = #{id} and name = #{name}
</select>
这里,#{id}
和 #{name}
分别代表 Map
参数中键为 "id"
和 "name"
的值。MyBatis 会在执行时自动从传入的 Map
中取出对应的值,并将其作为预编译参数插入 SQL 查询中,保证安全性。
3. 准备参数与调用查询
最后,在应用程序中创建一个 HashMap
实例,填充所需的 id
和 name
值,然后调用 Mapper 方法执行查询:
// 7.map集合方式输入id和姓名,查询用户
Map<String, Object> map = new HashMap<>();
map.put("id", 1);
map.put("name", "萨达姆");
User userByIdAndName2 = userMapper.findUserByIdAndName2(map);
if (userByIdAndName2 != null) {
System.out.println("查询成功");
System.out.println(userByIdAndName2); // 修改为 println 输出整行
} else {
System.out.println("没有数据");
}
这段代码首先创建了一个 HashMap
并放入 "id"
和 "name"
的键值对。然后,调用 userMapper.findUserByIdAndName2(map)
执行查询。根据查询结果,判断是否找到了匹配的用户,并打印相应的信息。
总结
通过上述步骤,我们展示了如何使用 Map
类型参数在 MyBatis 中实现多参数查询。关键要点如下:
- 使用
Map
集合封装多个查询参数,简化接口定义。 - 在 Mapper 接口中声明接受
Map
参数的方法。 - 在 XML 映射文件中,使用
#{mapKey}
格式引用Map
中的键值,确保 SQL 安全性。 - 在应用中创建
Map
实例,填充参数,调用 Mapper 方法执行查询,并处理查询结果。
这种做法尤其适用于参数数量不确定或变动频繁的场景,有助于保持代码的灵活性和可维护性。初学者在实际编程中可以借鉴此模式,根据需求适配自己的查询逻辑。
5. 实体类类型的参数
若mapper接口中的方法参数为实体类对象时 此时可以使用${}和#{},通过访问实体类对象中的属性名获取属性值,注意${}需要手动加单引号
在 MyBatis 中,当一个 Mapper 接口方法的参数为实体类对象时,可以直接使用实体类属性作为 SQL 语句中的占位符值。这种方式简化了参数传递,无需手动构建 Map 或其他数据结构。
以User实体类作为参数,添加用户
1. 定义 Mapper 接口方法
首先,在 UserMapper.java
中定义一个使用 User
实体类作为参数的方法,如:
public int addUser2(User user);
此方法表示我们要根据 User
实例的属性值来添加一个新的用户记录到数据库。
2. 编写 XML 映射文件
接着,在 UserMapper.xml
中编写对应的 SQL 插入语句,使用 #{属性名}
占位符来引用实体类对象的属性:
<!-- 8.以User实体类作为参数,添加用户 -->
<insert id="addUser2">
insert into user values(0, #{name}, #{age}, #{sex})
</insert>
这里,#{name}
, #{age}
, 和 #{sex}
分别代表 User
对象的 name
, age
, 和 sex
属性值。MyBatis 会在执行时自动从传入的 User
实例中取出对应的属性值,并将其作为预编译参数插入 SQL 插入语句中,保证安全性。
3. 准备参数与调用添加方法
最后,在应用程序中创建一个 User
实例,填充所需的属性值,然后调用 Mapper 方法执行添加操作:
// 8.以User实体类作为参数,添加用户
User user = new User(0, "小明", 18, "男");
int i = userMapper.addUser2(user);
if (i > 0) {
System.out.println("添加成功");
} else {
System.out.println("添加失败");
}
这段代码首先创建了一个 User
对象,并设置了 name
, age
, 和 sex
属性。然后,调用 userMapper.addUser2(user)
执行添加操作。根据返回的受影响行数判断添加是否成功,并打印相应的信息。
总结
通过上述步骤,我们展示了如何使用实体类类型参数在 MyBatis 中实现用户添加操作。关键要点如下:
- 直接使用实体类对象作为 Mapper 方法的参数,简化参数传递。
- 在 Mapper 接口中声明接受实体类对象的方法。
- 在 XML 映射文件中,使用
#{属性名}
格式引用实体类对象的属性,确保 SQL 安全性。 - 在应用中创建实体类实例,填充属性值,调用 Mapper 方法执行添加操作,并根据返回结果判断操作是否成功。
这种做法适用于参数与实体类属性紧密关联的场景,能够简化代码结构,提高代码的可读性和一致性。初学者在设计数据操作接口时,可以优先考虑使用实体类作为参数,以充分利用 MyBatis 对实体类属性的自动映射功能。
6. 使用@Param标识参数
可以通过@Param注解标识mapper接口中的方法参数 此时,会将这些参数放在map集合中,以@Param注解的value属性值为键,以参数为值;以 param1,param2...为键,以参数为值;只需要通过${}和#{}访问map集合的键就可以获取相对应的值, 注意${}需要手动加单引号
在 MyBatis 中,@Param
注解可以用于为 Mapper 接口方法的参数提供别名,特别是在方法包含多个参数时,有助于清晰地标识每个参数的作用,并在 XML 映射文件中通过别名引用它们。以下是如何使用 @Param
注解查询用户的步骤和示例:
使用@Param输入id和姓名,查询用户
1. 定义 Mapper 接口方法
首先,在 UserMapper.java
中定义一个带有 @Param
注解的方法,为 id
和 name
参数赋予别名:
// 9.使用@Param输入id和姓名,查询用户
public User findUserByIdAndName3(@Param("id") int id, @Param("name") String name);
这里,@Param("id")
和 @Param("name")
分别为 int id
和 String name
参数指定了别名 "id"
和 "name"
。这些别名将在 XML 映射文件中作为占位符的键来使用。
2. 编写 XML 映射文件
接着,在 UserMapper.xml
中编写对应的 SQL 查询语句,使用 #{别名}
占位符来引用带有 @Param
注解的参数:
<!--9.使用@Param输入id和姓名,查询用户-->
<select id="findUserByIdAndName3" resultType="User">
select * from user where id = #{id} and name = #{name}
</select>
这里,#{id}
和 #{name}
分别代表方法参数中被 @Param
注解标记为 "id"
和 "name"
的值。MyBatis 会在执行时自动从方法参数中取出对应的值,并将其作为预编译参数插入 SQL 查询中,保证安全性。
不写id和name的话,也可以以 param1,param2...为键,以参数为值;
3. 调用查询方法
最后,在应用程序中直接调用 Mapper 方法,传入 id
和 name
参数值:
// 9.使用@Param输入id和姓名,查询用户
User userByIdAndName3 = userMapper.findUserByIdAndName3(1, "萨达姆");
if (userByIdAndName3 != null) {
System.out.println("查询成功");
System.out.println(userByIdAndName3); // 修改为 println 输出整行
} else {
System.out.println("没有数据");
}
这段代码直接调用 userMapper.findUserByIdAndName3(1, "萨达姆")
,传入 id
和 name
的具体值。根据查询结果,判断是否找到了匹配的用户,并打印相应的信息。
总结
通过上述步骤,我们展示了如何使用 @Param
注解在 MyBatis 中实现多参数查询。关键要点如下:
- 使用
@Param
注解为 Mapper 接口方法的参数指定别名,增加代码可读性。 - 在 Mapper 接口中声明带有
@Param
注解的方法。 - 在 XML 映射文件中,使用
#{别名}
格式引用@Param
注解标记的参数,确保 SQL 安全性。 - 在应用中直接调用 Mapper 方法,传入相应参数值,并处理查询结果。
这种做法特别适用于方法参数较多且需要清晰标识其用途的场景,增强了代码的可理解性和维护性。初学者在编写多参数的 MyBatis 查询方法时,应优先考虑使用 @Param
注解来提升代码质量。注意,这里并不涉及将参数放入 Map
集合中,而是直接使用注解提供的别名与方法参数关联。