MyBatis04-MyBatis小技巧
一、#{}与${}
结论先行:
表达式 | 名字 | 安全性 | SQL 注入风险 | 常用于 | 底层 |
---|---|---|---|---|---|
#{} | 预编译占位符 | ✅ 安全 | ❌ 不会 | 参数值 | PreparedStatement |
${} | 字符串拼接 | ❌ 不安全 | ✅ 可能 | 表名、列名等动态 SQL | Statement |
#{}
是“给 SQL 填值”,${}
是“把参数原样拼到 SQL 里”。
1-1、解释:
1、#{}
:预编译,占位符(推荐使用)
作用:
-
用来传递参数值
-
MyBatis 会用
?
占位,底层通过PreparedStatement
设置参数,自动防止 SQL 注入。
示例:
<select id="getUserById" resultType="User">SELECT * FROM user WHERE id = #{id}
</select>
等价于 JDBC:
SELECT * FROM user WHERE id = ?
然后由 MyBatis 自动 set:
statement.setInt(1, id);
特点:
-
安全!
-
自动处理字符串加引号、类型转换等!!!!
2、${}
:字符串拼接(慎用!)
作用:
-
直接把参数拼接到 SQL 字符串中
-
没有
?
占位,MyBatis 不处理类型、不加引号!!! -
很容易出现 SQL 注入漏洞
【示例1】:
<select id="getTableData" resultType="Map">SELECT * FROM ${tableName}
</select>
如果调用:
mapper.getTableData("user");
生成的 SQL 是:
SELECT * FROM user
但如果有人传入:
mapper.getTableData("user; DROP TABLE user; --");
你的数据库就炸了!
【示例2】:
要是写的是:
<select id="getUserByName" resultType="Map">SELECT * FROM user where name = ${name}
</select>
此时代码会报错:
因为代码会直接对sql语句拼接,再编译:
select * from user where name = sam
直接传入值,不会做任何的处理!
1-2、应用场景对比:
场景 | 推荐用法 | 示例 |
---|---|---|
查询某个用户 | #{id} | WHERE id = #{id} |
动态指定表名、列名 | ${} | SELECT * FROM ${tableName} |
模糊查询(like) | #{} 配合 % | WHERE name LIKE CONCAT('%', #{keyword}, '%') |
1-3、模糊查询的正确方式:
<select id="searchUser" resultType="User">SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')
</select>
或者:
SELECT * FROM user WHERE name LIKE #{name}
调用时传入:
mapper.searchUser("%Alice%");
1-4、使用 ${}
的误区
SELECT * FROM user WHERE name = '${name}'
这样写看起来像加了引号,但其实没任何防护机制。如果 name
是 "Alice' OR 1=1 --"
,你就会中招。
1-5、总结:
对比点 | #{} | ${} |
---|---|---|
是否预编译 | ✅ 是 | ❌ 否 |
是否安全 | ✅ 是 | ❌ 否 |
是否防 SQL 注入 | ✅ 是 | ❌ 否 |
是否自动加引号 | ✅ 是 | ❌ 否 |
用途 | 传参数值 | 拼字段名、表名、排序字段等 |
实用建议:
80%以上的情况请用
#{}
,除非你明确知道你要拼的是表名、列名等结构,不是值!
二、MyBatis 中的 批量删除
2-1、常见的批量删除方式:
DELETE FROM user WHERE id IN (1, 2, 3);
我们只需要在 MyBatis 的 mapper XML 中写出动态拼接 id
列表的语句即可。
2-2、使用 <foreach>
实现批量删除
示例:批量删除用户
1、Mapper 接口
int deleteUsers(@Param("ids") List<Integer> ids);
2、对应 XML 映射
<delete id="deleteUsers">DELETE FROM user WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</delete>
示例调用
List<Integer> ids = Arrays.asList(1, 2, 3, 4);
mapper.deleteUsers(ids);
生成的 SQL 就是:
DELETE FROM user WHERE id IN (1, 2, 3, 4)
2-3、语法说明:
属性 | 含义 |
---|---|
collection | 要遍历的集合名,对应方法参数名或 @Param("xxx") 名 |
item | 每次迭代的变量名 |
open / close | 括号包围,生成 ( ... ) |
separator | 每个元素之间的分隔符,如逗号 |
2-4、补充:支持数组
如果你传的是数组:
int deleteUsers(int[] ids);
也同样能用 <foreach collection="ids">
遍历。
2-5、不推荐的错误方式:用 ${}
拼接
<delete id="deleteUsers">DELETE FROM user WHERE id IN (${ids})
</delete>
调用:
mapper.deleteUsers("1,2,3")
❌ 不安全、容易 SQL 注入,而且参数类型变成了字符串,控制不方便,不推荐!
三、mybatis 模糊查询
对应的模糊查询的sql:
SELECT * FROM user WHERE name LIKE '%Ali%'
3-1、在 SQL 中使用CONCAT + #{},拼接 %(推荐)
示例:
<select id="searchUser" resultType="User">SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')
</select>
java调用:
mapper.searchUser("Ali"); // 实际生成 SQL: name LIKE '%Ali%'
3-2、使用 ${}
拼接模糊查询(不推荐!不安全!)
SELECT * FROM user WHERE name LIKE '%${name}%'
这样会把参数原样拼进 SQL,能执行成功!但是,可能导致SQL 注入漏洞!
【注意】:
SELECT * FROM user WHERE name LIKE '%#{name}%'
这样写则会报错!
因为代码会先用?占位符代替,变为:select * from user where name like '%?%'
但是,在字符串中的?JDBC只会当做是普通的?字符串,不会填值!
3-3、推荐做法总结:
方式 | SQL 写法 | Java 传参 | 安全性 | 推荐 |
---|---|---|---|---|
SQL 中用 CONCAT | LIKE CONCAT('%', #{name}, '%') | "Ali" | ✅ 安全 | ✅ 推荐 |
传参时拼好 % | LIKE #{name} | "%Ali%" | ✅ 安全 | ✅ 也可用 |
直接用 ${} | LIKE '%${name}%' | "Ali" | ❌ 不安全 | ❌ 不推荐 |
3-4、模糊前缀、后缀查询
类型 | SQL 写法 |
---|---|
前缀匹配 | LIKE CONCAT(#{name}, '%') 例如:name LIKE 'Ali%' |
后缀匹配 | LIKE CONCAT('%', #{name}) 例如:name LIKE '%Ali' |
全部匹配 | LIKE CONCAT('%', #{name}, '%') 例如:name LIKE '%Ali%' |
四、 给 Java 类起一个简短的“别名”:<typeAlias>
标签
<typeAlias>
是用来给 Java 类起一个简短的“别名”,在 MyBatis 的 XML 文件中可以直接用别名代替类的全限定名。
4-1、使用前后的对比:
1、不使用 <typeAlias>
:
<select id="selectByActno" resultType="com.wsbazinga.bank.pojo.Account">select * from t_act where actno = #{actno}
</select>
resultType会写的很长!
2、使用 <typeAlias>
起别名:
【注意】:
标签的位置有约束,不对的话,控制台会报错!
4-2、使用方法详解:
在 mybatis-config.xml
中配置。
1、单个别名配置:
2、一整个包下的类自动起别名:
自动起别名时,MyBatis 会默认使用 类名的小写首字母形式作为别名,例如:
com.example.domain.User
→ 别名是"user"
(不区分大小写)
3、小细节:
-
alias="..."
是你起的别名,可以自己定义(区分大小写与否取决于 MyBatis 版本和配置) -
如果你不写
alias
,MyBatis 默认用类名(首字母小写) -
自动注册包时,只会注册普通的 JavaBean 类(带 getter/setter)
4、使用场景:
使用位置 | 说明 |
---|---|
<resultType> | 查询结果返回值类型 |
<parameterType> | SQL 参数类型 |
<resultMap> 的 type 属性 | 映射对象类型 |
【注意】:
namespace没有别名,必须写接口的全限定名称!
5、高级补充(可选):
你还可以在类上加注解来自定义别名:
@Alias("User")
public class User {...
}
需要依赖 MyBatis 的注解支持(比如在 Spring 中使用)
6、Spring Boot + MyBatis 项目
在 Spring Boot + MyBatis 项目中,确实不需要手动写 <typeAlias>
标签了——MyBatis-Spring-Boot-Starter 帮你做了这件事!
在 Spring Boot 中,你可以通过配置 + 注解,自动扫描实体类并注册别名,根本不需要手动写 <typeAlias>
。
(1)、方法一:使用 @MapperScan
+ 自动扫描别名(推荐方式)
Spring Boot 的 mybatis-spring-boot-starter
支持在 application.yml
或 application.properties
中配置扫描别名的包路径:
application.yml
示例:
mybatis:type-aliases-package: com.example.domain
自动将
com.example.domain
包下所有类注册为别名,默认是类名(不区分大小写)
同时,在启动类或配置类加上扫描 Mapper 接口:
@MapperScan("com.example.mapper")
@SpringBootApplication
public class MyApp {public static void main(String[] args) {SpringApplication.run(MyApp.class, args);}
}
(2)方法二:使用注解 @Alias
给类手动指定别名(可选)
你也可以在实体类上加注解:
import org.apache.ibatis.type.Alias;@Alias("User")
public class User {private int id;private String name;// getter/setter
}
如果用了
@Alias
,MyBatis 会优先使用你定义的别名。
五、mybatis-config.xml中的<mapper>标签
<mapper>
标签的属性有三个:resource
、url
或class
三种方式之一。
<mappers>
标签有三种子标签写法
<mappers><!-- 1. 使用 class 属性 --><mapper class="com.example.mapper.UserMapper"/><!-- 2. 使用 resource --><mapper resource="mapper/UserMapper.xml"/><!-- 3. 使用 url --><mapper url="file:///absolute/path/to/mapper/UserMapper.xml"/>
</mappers>
写法 | 含义 | 用途 |
---|---|---|
class | 指定一个 Mapper 接口(Java 类) | 适用于使用注解的 Mapper 或注解+XML 双结合 |
resource | 指定一个 XML 文件路径(类路径下) | 适用于只有 XML 的 SQL 映射 |
url | 指定一个 XML 文件的绝对路径 | 不推荐!主要用于外部加载,绝对路径,可扩展性差! |
5-1、重点解释:<mapper class="...">
示例:
<mappers><mapper class="com.example.mapper.UserMapper"/>
</mappers>
含义:
-
这是一个 Mapper 接口(Java 接口)
-
MyBatis 会根据接口名去找对应的 XML(如有)或注解(此时XML文件要和Mapper接口在同一个目录下!)
5-2、绑定规则(很重要):
当你使用 <mapper class="...">
方式时:
1、xxxMapper接口和xxxMapper.xml文件在同一个目录下;
2、名字一致!即:carMapper接口---> carMapper.xml文件
5-3、使用场景举例:
接口 + XML(常见方式)
public interface CarMapper {Car selectById(String carNum);}
<!-- UserMapper.xml -->
<mapper namespace="com.wsbazinga.bank.dao.CarMapper"><select id="selectById" resultType="com.wsbazinga.bank.pojo.Car">selectid,car_num as carNum,brand,guide_price as guidePrice,produce_time as produceTime,car_type as carTypefrom t_carwhere id = #{id}</select>
</mapper>
注册方式:
<mappers><mapper class="com.wsbazinga.bank.dao.CarMapper"/>
</mappers>
【注意】:Mapper接口和Mapper.xml文件的位置
目录的结构是一样的,只是一个是java根目录,一个是resources根目录,但是编译之后:
在同一个目录下!
提醒!!!!!!!!!!!!!!!!!!!!!!
在IDEA的resources目录下新建多重目录的话,必须是这样创建:
com/powernode/mybatis/mapper 不能这样: com.powernode.mybatis.mapper(这样是单层的)
因为resources目录下只能创建directory不能创建package!
5-4、拓展
若是有多个mapper文件,不需要每一个都写,直接使用<package>
六、Mybatis获取插入数据后自增的主键
6-1、业务场景:
6-2、实现方式:使用 useGeneratedKeys
和 keyProperty属性
示例:
属性 | 含义 |
---|---|
useGeneratedKeys="true" | 表示使用 JDBC 的自动主键返回功能 |
keyProperty="id" | 表示把返回的主键值赋值给 User 对象中的 id 属性 |
这要求你数据库表中
id
是自增主键(如AUTO_INCREMENT
)
6-3、Mybatis-plus的实现方式
前提是你用的是 MyBatis-Plus,而不是纯 MyBatis。
@TableId(type = IdType.ASSIGN_ID)
这个注解是 MyBatis-Plus 的主键策略配置,不适用于原生 MyBatis!
这个注解用于告诉 MyBatis-Plus:这个字段是主键,且主键生成策略是什么。
1、常用主键生成策略类型:
策略 | 含义 | 说明 |
---|---|---|
AUTO | 数据库自增(默认) | MySQL 的 AUTO_INCREMENT |
INPUT | 手动输入 | 主键由你自己赋值 |
ASSIGN_ID | 自动生成(雪花算法) | 推荐方式,默认用雪花算法生成全局唯一 ID(19位长整型) |
ASSIGN_UUID | 自动生成 UUID | 用 UUID(适合主键是字符串) |
NONE | 不设置策略 | 通常不建议用 |
2、示例代码:
@Data
public class User {@TableId(type = IdType.ASSIGN_ID)private Long id;private String name;private String email;
}
然后你在使用 userMapper.insert(user)
时,MyBatis-Plus 会自动为你填充 id
值(不依赖数据库自增)
3、特点:@TableId
项目 | @TableId(type = …) |
---|---|
所属框架 | ✅ MyBatis-Plus 专属 |
依赖数据库主键 | ❌ 不依赖数据库自增 |
是否需要数据库生成主键 | ❌ MyBatis-Plus自动生成 |
推荐使用场景 | 分布式系统、主键冲突多的业务 |