#和的区别是什么?什么情况必须用
#{}:安全、预编译、自动加引号、防 SQL 注入 → 99% 场景都用它
${}:直接拼接、不安全、不处理、可能被 SQL 注入 → 只有特殊场景才用
SELECT * FROM user WHERE id = #{userId}
执行的时候是变成:
SELECT * FROM user WHERE id = ?
SELECT * FROM user WHERE id = ${userId}
执行的时候变成:
SELECT * FROM user WHERE id = 101
可以发现#{}会编译成占位符的形式,然后执行的时候自动把值传递进去,如果发现是字符串类型的话会自动加上''
SQL注入的场景:
SELECT * FROM user WHERE name = '${name}'
用户输入:' OR '1'='1
拼接出来变成:SELECT * FROM user WHERE name = '' OR '1'='1'
比如使用${} 的场景
当传递的不是值,而是一些特殊的字面量,比如SQL的关键字等
SELECT * FROM ${tableName}
ORDER BY ${sortField} ${sortType}
ORDER BY create_time DESC
SELECT * FROM ${dbName}.user
如果一定需要使用${} 的话,需要从几个地方来共同保证安全:
- 前端需要对输入格式进行处理
- 后端再做一层校验
- 最后传递给数据库的时候,不要直接把用户传递进来的给人带进来,通过枚举类来传递
MyBatis-Plus的分页原理是什么?
Page<User> page = new Page<>(1, 10); // 第1页,每页10条
userMapper.selectPage(page, queryWrapper);
当我们写了上面这个代码之后,MP底层会通过拦截器拦截下来之后,自动拼接LIMIT分页
比如变成:
SELECT * FROM user LIMIT 0,10
可以发现MP的分页其实是物理分页,不是逻辑分页。
物理分页就是真的只查询出10条,而不是查询出全部,再计算其中的10条返回
拦截器会拦截所有的查询sql,并判断是否需要进行分页
分页查询的时候,MP会自动执行count,得到对应的总记录数
使用MP的配置:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
分页返回的 IPage :
page.getRecords() // 当前页数据列表
page.getCurrent() // 当前页码
page.getSize() // 每页条数
page.getTotal() // 总条数
page.getPages() // 总页数
MyBatis-Plus有什么用?
就是MyBatis 的封装版,可以帮你省去手写sql和手写xml的过程
它对Mybatis只做增强,不做改变,底层其实就是在Java代码中动态生成sql,然后交给Mybatis去执行
对用户暴露出丰富的API,比如Wrapper,以及Lambda格式,使用MP更安全,不容易写错字段名
提供了分页插件,能自动完成分页查询
Mybatis插件的运行原理?
插件就是在sql执行前后拦截住,改sql,改参数,改结果
插件涉及到几个接口:
- Interceptor:拦截器接口,定义了Mybatis插件的基本功能,包括插件的初始化、插件的拦截方法以及插件的销毁方法。
- Invocation:调用接口,表示Mybatis在执行SQL语句时的状态,包括SQL语句、参数、返回值等信息。
- Plugin:插件接口,Mybatis框架在执行SQL语句时,会将所有注册的插件封装成Plugin对象,通过Plugin对象实现对SQL语句的拦截和修改。
demo:
@Intercepts({
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
@Component
public class DelFlagInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
// 在这里修改 SQL
String newSql = sql + " and del_flag = 0";
// 反射把新SQL塞回去
Field field = boundSql.getClass().getDeclaredField("sql");
field.setAccessible(true);
field.set(boundSql, newSql);
// 放行
return invocation.proceed();
}
}
插件的运行流程:
- 首先,当Mybatis框架运行时,会将所有实现了Interceptor接口的插件进行初始化。
- 初始化后,Mybatis框架会将所有插件和原始的Executor对象封装成一个InvocationChain对象。(这里使用的是责任链模式,也就是把你自己封装的插件和Executor对象一起封装到同一条责任链上)
- 每次执行SQL语句时,Mybatis框架都会通过InvocationChain对象依次调用所有插件的intercept方法,实现对SQL语句的拦截和修改。
- 最后,Mybatis框架会将修改后的SQL语句交给原始的Executor对象执行,并将执行结果返回给调用方。
Mybatis的工作原理?
启动阶段:
加载所有的xml和注解,把这些sql加载到内存中缓存起来,
运行阶段:
把你调用api的代码生成代理对象
代理对象通过接口名+方法查询具体的sql
把sql交给executer执行
executer 先查询二级缓存,未命中,查询一级缓存,均未命中则查库
结果映射转换之后返回(封装成对象返回)
Mapper 接口没有实现类,为什么能调用?
因为 JDK 动态代理生成了代理对象,方法调用走代理。
MyBatis 插件为什么能拦截?
因为 Executor、StatementHandler 都是代理对象,插件拦截它们。
MyBatis 怎么把结果转成对象?
ResultSetHandler + 反射 + 自动映射 / ResultMap
Mybatis的缓存机制
SqlSession 就是和数据库的一次连接会话,只会包含一次事务,事务提交就结束
也就是 SqlSession 就是和事务绑定的
**一级缓存:**同一个sqlSesseion里的临时缓存
默认就是自动开启的
一级缓存其实就是一个HashMap,key就是sql,value就是结果
什么时候生效?
同一个 SqlSession 里:
- 执行相同 SQL
- 参数相同
- 没有执行 insert/update/delete
- 没有手动清空缓存
第二次查询直接走缓存,无需查库
什么时候失效?
只要发生下面任意一种,缓存立刻清空:
- 执行了 insert / update / delete
- 事务 提交 / 关闭
- 调用了 clearCache()
- 到了超时时间
二级缓存: 跨 SqlSession,整个 Mapper 共享的缓存
特点:
- 默认关闭,需要手动开启
- 以Mapper 命名空间为单位(一个 Mapper 一个缓存)
- 多个请求、多个会话共用一份缓存
工作流程:查询的时候先走二级缓存,未命中才走一级缓存,还不命中就进行查库
为什么不推荐用?
致命问题:脏数据!
例如:
- UserMapper 联表查询 user + order
- 结果被存在 UserMapper 的二级缓存里
- 你去 OrderMapper 更新了订单表
- UserMapper 的缓存根本不知道!
- 下次查询 → 读到旧数据 → 脏数据!
二级缓存一般是不用的,容易出现脏数据
一级缓存为什么得是sqlSession级别的?
sqlSession本质上就是一个事务内部的,所以一级缓存其实也是和事务绑定的
它是为了在当前事务内执行相同的sql能快速的返回,并且执行写操作的时候,这个缓存可以感知并失效
但是会存在一个问题就是可能读取到脏数据,因为你操作的时候别的事务同样可以修改,那么你缓存的数据就变成了旧数据
但是这个是不可避免的,它和mysql原生的RR隔离级别的要求是一样的,读取到旧版本的数据
Mybatis的优点有哪些?
简化JDBC,底层已经把复杂的sql操作封装好,并且支持#{}这种预编译的方式
不同自己去手动管理连接之类的
Mybatis可以实现动态SQL么?
可以。几个常见的标签
<select id="getUser" resultType="User">
SELECT * FROM user
WHERE 1=1
<if test="name != null and name != ''">
AND name LIKE CONCAT('%',#{name},'%')
</if>
<if test="age != null">
AND age = #{age}
</if>
</select>
只有传递了对应的参数才会拼接上来
<select id="getUser" resultType="User">
SELECT * FROM user
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="age != null">
AND age = #{age}
</if>
</where>
</select>
取代掉1= 1
1 = 1原本是为了避免上一个判断为空,后一个判断有值,
就会变成:SELECT*FROM user WHERE AND age = ?
<select id="listByIds" resultType="User">
SELECT * FROM user
WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</select>
循环匹配
Mybatis是否支持延迟加载?实现原理是什么?
支持,但是默认不开启
延迟加载就是当一个对象关联了另外一个对象,当我们查询这个对象的时候,如果直接把关联对象也查询出来,就不是延迟加载,真正的延迟加载是要用到的时候才触发查询
public class Order {
private int id;
private String orderNumber;
private List<Item> items;
}
public class Item {
private int id;
private int orderId;
private String itemName;
private BigDecimal price;
}
Item 应该在要用到的时候检查是否已经加载,没加载的话触发额外加载逻辑
原理就是,当我们开启了延迟加载的时候,查询主对象的时候,会返回一个代理对象给调用者,当后面需要关联这些对象的时候,代理对象检查是否已经加载,未加载会触发额外的加载逻辑
Mybatis是如何实现字段映射的?
其实就是把sql执行结果的 ResultSet 通过ResultSetHandler进行字段设置
按 "列名 ------ 属性名" 匹配,用反射 set 进对象,完成字段映射
映射方式:
-
自动同名/驼峰转换
SELECT id, user_name, age FROM user
private Long id;
private String userName;
private Integer age; -
sql里面起别名
如果实在列名和属性名实在匹配不上的话,可以通过在sql里面通过AS起别名的方式
SELECT username AS nick_name FROM user
private String nickName;
- 通过xml
这种适合完全映射不上的时候,可以你自己来指定匹配规则
<resultMap id="userMap" type="User">
<id column="id" property="id"/>
<result column="user_name" property="name"/>
<result column="age" property="userAge"/>
</resultMap>
映射流程:
- Mybatis通过JDBC API向数据库发送SQL查询语句,并获得查询结果集。
- 查询结果集中的所有数据封装到一个ResultSet对象中 ,Mybatis遍历ResultSet对象中的数据。
- 对于每一行数据,Mybatis根据Java对象属性名和查询结果集中的列名进行匹配。如果匹配成功,则将查询结果集中的该列数据映射到Java对象的相应属性中。
- 如果Java对象属性名和查询结果集中的列名不完全一致,Mybatis可以通过在SQL语句中使用"AS"关键字或使用别名来修改列名,或者使用ResultMap来定义Java对象属性和列的映射关系。
- 对于一些复杂的映射关系,例如日期格式的转换、枚举类型的转换等,可以通过自定义TypeHandler来实现。Mybatis将自定义TypeHandler注册到映射配置中,根据Java对象属性类型和查询结果集中的列类型进行转换。
- 最终,Mybatis将所有映射成功的Java对象封装成一个List集合,返回给用户使用。
Mybatis用的什么连接池?
有自带,自带的默认是 PooledDataSource
但是这个连接池:
- 功能简单
- 没有监控
- 没有高级策略(心跳、保活、泄漏检测等)
- 性能不如专业连接池
所以时机项目中一般不用
真实项目一般使用:
- HikariCP(SpringBoot 默认,最快)
- Druid(阿里,监控强大)
使用MyBatis如何实现分页?
分页有物理分页和逻辑分页两种
物理分页就是使用limit进行截取,而物理分页是每次查询全部,然后再进行逻辑分页
默认有四种:
- 手写SQL,通过Limit进行分页
- 通过 PageHelper 插件 ,底层也是拼接limit,同样是物理分页
- 通过 RowBounds,是逻辑分页,每次会查询全表,然后内存截取,已经废弃掉
- 通过MP的分页插件,同样是物理分页
MyBatis #{} 传入实体对象、List 集合、数组,XML 怎么解析?#{} parameterType、resultType、resultMap 区别?一对多、多对一怎么映射?
传入单个实体对象
select * from user where id = #{id} and name = #{name}
当#{} 传入单个实体给上面这个sql,那这个sql执行的时候会自动调用对应的get()方法
比如调用 user.getId()、user.getName()
传入List集合和数组
select * from user where id in
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
select * from user where id in
<foreach collection="array" item="id" open="(" separator="," close=")">
#{id}
</foreach>
使用foreach遍历拼接,只不过集合类型需要区分一下list还是array
底层解析成sql的时候会自动解析成 in(x,x,x)的形式
parameterType、resultType、resultMap 区别
parameterType:传入参数类型
- 可以写实体、Map、基本类型、List
- 可省略,MyBatis 能自动推断
例如:
parameterType="com.xx.User"
resultType:单表 / 简单返回用
- 自动按 字段名 = 属性名 映射
- 不支持复杂映射(一对多、多对一)
例如:
resultType="com.xx.User"
resultType="java.lang.Integer"
resultMap:复杂结果映射专用
- 手动指定字段→属性、别名、关联映射(一对多、多对一)
- 功能最强,最常用
例如:
resultMap="userResultMap"
多对一怎么映射?
<resultMap id="userMap" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 多对一 -->
<association property="dept" javaType="Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
</association>
</resultMap>
使用association标签,一般就是用来映射内部实体
一对多怎么映射?
<resultMap id="deptMap" type="Dept">
<id column="dept_id" property="id"/>
<result column="dept_name" property="name"/>
<!-- 一对多 -->
<collection property="userList" ofType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</collection>
</resultMap>
使用collection
MyBatis 底层执行流程:SqlSession、Executor、MapperProxy 代理、StatementHandler 四大组件流程?MyBatis 一级缓存存在于哪个组件里?
MyBatis底层完整执行流程:
- 加载配置,程序启动的时候会加载 mybatis-config.xml、Mapper.xml,构建 Configuration。
- 创建会话工厂( SqlSessionFactory ),根据 Configuration 构建会话工厂。
- 创建sqlSession,一个连接等于一个sqlSession,里面包含事务,Executor
- 获取 Mapper 代理 ( MapperProxy ),调用 getMapper (XXXMapper.class) 时,MyBatis 用 JDK 动态代理 生成 MapperProxy 代理对象。
- 执行mapper方法,当业务调用mapper代码的时候, 代理类调用 SqlSession 的 selectOne /update 等方法。 (也就是说这里不是业务方在调用,而是底层的代理类在调用)
- SqlSession 把sql 交给 Executor 执行 ,Executor 是真正的执行器。负责:一级缓存、事务管理、调用 StatementHandler
- StatementHandler 处理 JDBC ,获取 Connection,创建 Statement / PreparedStatement,设置参数(ParameterHandler),执行 SQL,封装结果(ResultSetHandler)
各个组件都是做什么的?
完整调用链路: MapperProxy → SqlSession → Executor → StatementHandler → JDBC
MapperProxy代理 Mapper 接口,拦截方法调用,转入 SqlSession。
SqlSession对外 API 入口,统一调度,内部持有 Executor。
Executor 核心执行器,负责缓存、事务、调度 StatementHandler。
StatementHandler最底层,和 JDBC 直接交互,处理参数、执行、结果映射。
所以一级缓存是存储在Executor中的,此外事务也是这个区域来负责,而StatementHandler是底层和JDBC打交道的
MyBatis #{} 传入 Integer 0、空字符串 null,MySQL 查询会出现什么坑?where if 标签非空判断 test="xxx!=null and xxx!=''" 为什么必写?数字类型特有 bug 是什么?
Integer 0 的坑 :
<if test="status != null">
and status = #{status}
</if>
MyBatis 在 OGNL 表达式中:数字 0、空字符串、false 都会被判定为 "假"
所以上面的sql会直接忽略status,直接不拼接
空字符串 '' 的坑 :
and age = #{age}
传入空字符串 '' 给数值类型(int、bigint),字符类型(包括空串)转化成数值类型会直接变成0
结果
and age = '' ,在mysql底层就变成age = 0
null 的坑 :
and status = #{status}
这里如果传递的是null,就变成
and status = null
但是在mysql里面要判断是否为null必需使用 is null ,如果用 status = null 判断结果一定是false
所以当字段是数字类型的时候,一定不能用 status != ' ' ,这个时候传递0,就变成 0 != ' '
然后 ' ' 会被转化成0,所以匹配结果其实是false,导致条件被忽略掉
Spring 整合 MyBatis 之后,一级缓存为什么几乎失效?事务传播级别对缓存影响?
Spring 整合 MyBatis 后,一级缓存之所以几乎失效,是因为 Spring 使用 SqlSessionTemplate 管理会话,每次 DAO 调用都会创建新的 SqlSession 并立即关闭,而一级缓存是 SqlSession 级别的,所以无法命中。
只有在 Spring 事务内部,会话会被绑定到当前线程并复用,一级缓存才会生效。
事务传播级别会影响缓存:
- REQUIRED、NESTED 会复用会话,缓存有效;
- REQUIRES_NEW 会新建会话,缓存独立隔离;
- 非事务环境或 NOT_SUPPORTED,每次新建会话,一级缓存完全失效。
MyBatis 映射文件 #{} 预编译底层原理,JDBC PreparedStatement 执行流程${} 为什么无法走预编译、一定会 SQL 注入?
MyBatis 的 #{} 底层会解析为 JDBC 预编译的 ? 占位符,使用 PreparedStatement 执行。执行流程是:先发送 SQL 模板给数据库编译固定结构,再传入参数,参数只作为数据处理,不会改变语法结构,因此可以防止 SQL 注入。
而 ${} 是直接字符串拼接,在 MyBatis 内部就把参数拼进 SQL 里,再发给数据库执行,无法走预编译。因为参数参与了 SQL 语法结构生成,攻击者可以构造恶意字符串改变 SQL 逻辑,所以一定会存在 SQL 注入风险。