MyBatis 常见问题总结

#和的区别是什么?什么情况必须用

#{}:安全、预编译、自动加引号、防 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

如果一定需要使用${} 的话,需要从几个地方来共同保证安全:

  1. 前端需要对输入格式进行处理
  2. 后端再做一层校验
  3. 最后传递给数据库的时候,不要直接把用户传递进来的给人带进来,通过枚举类来传递

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,改参数,改结果

插件涉及到几个接口:

  1. Interceptor:拦截器接口,定义了Mybatis插件的基本功能,包括插件的初始化、插件的拦截方法以及插件的销毁方法。
  2. Invocation:调用接口,表示Mybatis在执行SQL语句时的状态,包括SQL语句、参数、返回值等信息。
  3. 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();
    }
}

插件的运行流程:

  1. 首先,当Mybatis框架运行时,会将所有实现了Interceptor接口的插件进行初始化。
  2. 初始化后,Mybatis框架会将所有插件和原始的Executor对象封装成一个InvocationChain对象。(这里使用的是责任链模式,也就是把你自己封装的插件和Executor对象一起封装到同一条责任链上)
  3. 每次执行SQL语句时,Mybatis框架都会通过InvocationChain对象依次调用所有插件的intercept方法,实现对SQL语句的拦截和修改。
  4. 最后,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 里:

  1. 执行相同 SQL
  2. 参数相同
  3. 没有执行 insert/update/delete
  4. 没有手动清空缓存

第二次查询直接走缓存,无需查库

什么时候失效?

只要发生下面任意一种,缓存立刻清空:

  • 执行了 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 进对象,完成字段映射

映射方式:

  1. 自动同名/驼峰转换

    SELECT id, user_name, age FROM user

    private Long id;
    private String userName;
    private Integer age;

  2. sql里面起别名

如果实在列名和属性名实在匹配不上的话,可以通过在sql里面通过AS起别名的方式

复制代码
SELECT username AS nick_name FROM user

private String nickName;
  1. 通过xml

这种适合完全映射不上的时候,可以你自己来指定匹配规则

复制代码
<resultMap id="userMap" type="User">
    <id column="id" property="id"/>
    <result column="user_name" property="name"/>
    <result column="age" property="userAge"/>
</resultMap>

映射流程:

  1. Mybatis通过JDBC API向数据库发送SQL查询语句,并获得查询结果集
  2. 查询结果集中的所有数据封装到一个ResultSet对象中 ,Mybatis遍历ResultSet对象中的数据
  3. 对于每一行数据,Mybatis根据Java对象属性名和查询结果集中的列名进行匹配。如果匹配成功,则将查询结果集中的该列数据映射到Java对象的相应属性中。
  4. 如果Java对象属性名和查询结果集中的列名不完全一致,Mybatis可以通过在SQL语句中使用"AS"关键字或使用别名来修改列名,或者使用ResultMap来定义Java对象属性和列的映射关系。
  5. 对于一些复杂的映射关系,例如日期格式的转换、枚举类型的转换等,可以通过自定义TypeHandler来实现。Mybatis将自定义TypeHandler注册到映射配置中,根据Java对象属性类型和查询结果集中的列类型进行转换。
  6. 最终,Mybatis将所有映射成功的Java对象封装成一个List集合,返回给用户使用。

Mybatis用的什么连接池?

有自带,自带的默认是 PooledDataSource

但是这个连接池:

  • 功能简单
  • 没有监控
  • 没有高级策略(心跳、保活、泄漏检测等)
  • 性能不如专业连接池

所以时机项目中一般不用

真实项目一般使用:

  • HikariCP(SpringBoot 默认,最快)
  • Druid(阿里,监控强大)

使用MyBatis如何实现分页?

分页有物理分页和逻辑分页两种

物理分页就是使用limit进行截取,而物理分页是每次查询全部,然后再进行逻辑分页

默认有四种:

  1. 手写SQL,通过Limit进行分页
  2. 通过 PageHelper 插件 ,底层也是拼接limit,同样是物理分页
  3. 通过 RowBounds,是逻辑分页,每次会查询全表,然后内存截取,已经废弃掉
  4. 通过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底层完整执行流程:

  1. 加载配置,程序启动的时候会加载 mybatis-config.xml、Mapper.xml,构建 Configuration
  2. 创建会话工厂( SqlSessionFactory ),根据 Configuration 构建会话工厂。
  3. 创建sqlSession,一个连接等于一个sqlSession,里面包含事务,Executor
  4. 获取 Mapper 代理 ( MapperProxy ),调用 getMapper (XXXMapper.class) 时,MyBatis 用 JDK 动态代理 生成 MapperProxy 代理对象。
  5. 执行mapper方法,当业务调用mapper代码的时候, 代理类调用 SqlSession 的 selectOne /update 等方法。 (也就是说这里不是业务方在调用,而是底层的代理类在调用)
  6. SqlSession 把sql 交给 Executor 执行 ,Executor 是真正的执行器。负责:一级缓存、事务管理、调用 StatementHandler
  7. 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 注入风险。

相关推荐
qq_334563551 小时前
开发者工具怎么看HTML_Elements面板使用指南【操作】
jvm·数据库·python
m0_716430071 小时前
c++怎么读取安卓系统Assets目录下的资源文件流数据【实战】
jvm·数据库·python
大江东去浪淘尽千古风流人物1 小时前
【DROID-W】WildGS-SLAM
数据库·人工智能·python·oracle·augmented reality
清心歌1 小时前
LinkedList 深入解析
java
zhangchaoxies1 小时前
C#怎么实现MVVM模式 C#如何在WPF中使用MVVM设计模式分离视图和逻辑【架构】
jvm·数据库·python
吕源林1 小时前
防止SQL注入的应用层过滤_采用成熟的安全过滤中间件
jvm·数据库·python
鱼鳞_1 小时前
Java学习笔记_Day32(IO流字符集字符流)
java·笔记·学习
Rsun045511 小时前
17、Java 责任链模式从入门到实战
java·python·责任链模式
m0_747854521 小时前
Go语言如何做图算法_Go语言图算法实现教程【对比】
jvm·数据库·python