当前位置: 首页 > news >正文

MyBatis常见面试题

MyBatis常见面试题

一、基础概念类

1. 什么是 MyBatis?它和 JDBC 有什么区别?

什么是 MyBatis?
MyBatis 是一款优秀的半自动化的持久层框架,它封装了几乎所有的 JDBC 底层操作,简化了开发。它的核心思想是将程序中的 SQL 语句剥离出来,配置在 XML 文件或注解中,从而实现 SQL 与 Java 代码的解耦。它通过一套简单的配置,将 Java 对象(POJO)与数据库中的记录进行映射(ORM)。

它和 JDBC 有什么区别?

特性JDBC (Java Database Connectivity)MyBatis
本质Java 访问数据库的标准 API,是基础。基于 JDBC 的封装和扩展,是一个框架。
SQL 处理需要程序员手动编写、拼接复杂的 SQL 字符串在 Java 代码中,容易出错且难以维护。SQL 与 Java 代码分离,写在 XML 或注解中,清晰易管理。支持动态 SQL,灵活应对复杂查询。
结果集处理需要手动编写代码从 ResultSet 中遍历并一个个字段地取出数据,再塞到 Java 对象中,非常繁琐。自动进行 ORM 映射,通过 <resultMap> 或约定,直接将查询结果映射成 Java 对象,无需手动处理。
性能原生性能最高,但需要开发者自己优化(如连接池、预编译等)。性能接近 JDBC,因为它只是封装了 JDBC。它内置了连接池、预编译语句等优化,开箱即用。
开发效率。需要大量样板代码(注册驱动、获取连接、创建语句、处理异常、关闭资源等)。。大大减少了冗余代码,开发者只需关注 SQL 和业务逻辑。
可移植性差,SQL 硬编码在 Java 文件中,更换数据库可能需要修改代码。较好,SQL 在配置文件中,更换数据库时主要修改数据库连接配置和少量特定 SQL 语法。

总结:MyBatis 是 JDBC 的“增强版”,它解决了 JDBC 的硬编码、高冗余和繁琐结果集映射等问题,极大地提高了开发效率和代码可维护性。

2. MyBatis 的核心组件有哪些?

  1. SqlSessionFactoryBuilder (构造器)
    • 作用: 用于构建 SqlSessionFactory 对象。
    • 生命周期: 最佳作用域是方法作用域。一旦创建了 SqlSessionFactory,它就可以被丢弃。
  2. SqlSessionFactory (会话工厂)
    • 作用: 用于创建 SqlSession 实例。它是线程安全的,一旦被创建,在整个应用运行期间都应该存在。
    • 生命周期: 应用作用域(Application Scope)。通常通过单例模式来管理,一个数据库对应一个 SqlSessionFactory
  3. SqlSession (会话)
    • 作用: 是 MyBatis 的核心接口,包含了执行 SQL、管理事务、获取 Mapper 等方法。它代表了一次与数据库的会话。
    • 生命周期请求或方法作用域。它不是线程安全的!每次收到一个请求,就打开一个 SqlSession,请求处理完毕,必须及时关闭(通常放在 finally 块或使用 try-with-resources 语法)。
  4. Executor (执行器)
    • 作用: MyBatis 的底层核心,真正负责执行 SQL 语句。SqlSession 只是门面,所有数据库操作最终都是通过 Executor 完成的。它负责缓存、动态SQL参数处理等。
    • 类型SIMPLE(默认), REUSE, BATCH
  5. MappedStatement (映射语句)
    • 作用: 它封装了 MyBatis 执行语句的所有信息,例如 SQL 语句、输入参数映射、输出结果映射、缓存配置等。每一个 <select|insert|update|delete> 标签都会被解析成一个 MappedStatement 对象。
  6. MapperProxy (Mapper 代理)
    • 作用: Mapper 接口并没有实现类,MyBatis 使用 JDK 动态代理为 Mapper 接口生成代理对象。当我们调用 UserMapper.selectById() 方法时,实际上调用的是 MapperProxyinvoke 方法,它会找到对应的 MappedStatement 并执行。

3. MyBatis 的优缺点?适合什么场景?

优点:

  1. SQL 灵活可控: SQL 由开发者编写,可以针对性地进行优化,满足高性能、复杂查询的需求。
  2. 学习成本低: 相比于 Hibernate,MyBatis 更容易上手,只要会写 SQL 就能快速使用。
  3. 与 JDBC 平滑过渡: 性能接近原生 JDBC,且能直接使用数据库的特有功能。
  4. 良好的解耦: SQL 与 Java 代码分离,代码更清晰,易于维护。

缺点:

  1. SQL 工作量大: 需要手动编写所有 SQL 语句和结果映射,当表字段多时,比较繁琐。
  2. 数据库移植性差: SQL 是依赖特定数据库的,如果更换数据库,可能需要重写或调整大量 SQL。
  3. 半自动化: 不像全自动 ORM 框架那样完全屏蔽数据库,开发者仍需对数据库有一定的了解。

适合场景:

  • 需求多变、性能要求高的项目: 如互联网项目,业务逻辑复杂,经常需要优化 SQL。
  • 遗留系统或对 SQL 有特殊要求的项目: 需要充分利用数据库特性或处理复杂存储过程。
  • 团队 SQL 能力较强: 团队更倾向于直接控制和优化 SQL。

4. MyBatis 与 Hibernate 的主要区别?

维度MyBatisHibernate
ORM 程度半自动化,需要开发者编写 SQL。全自动化,完全封装 JDBC,开发者无需关心 SQL。
SQL 控制灵活可控,SQL 优化方便。失去控制权,由框架生成 SQL,复杂查询优化困难。
性能在复杂、大数据量查询中,由于 SQL 可优化,性能通常更好。在小批量、简单 CRUD 场景下效率高。复杂查询可能产生 N+1 问题,性能较差。
学习成本低,易于上手。高,需要掌握 HQL、Criteria API、缓存机制等。
数据库移植性,SQL 与数据库绑定。,使用 HQL 和方言,更换数据库成本低。
开发效率在简单 CRUD 上不如 Hibernate,在复杂查询上效率高。在标准 CRUD 场景下开发效率极高。
核心思想SQL 映射,围绕 SQL 开展工作。对象关系映射,围绕对象开展工作。

形象比喻:MyBatis 像手动挡汽车,驾驶者可以精准控制速度和性能;Hibernate 像自动挡汽车,驾驶简单,但极限操控不如手动挡。

5. MyBatis 是如何实现 SQL 映射的?

MyBatis 的 SQL 映射是其最核心的功能,主要通过以下几步实现:

  1. 解析阶段(启动时)
    • MyBatis 在应用启动时,会解析全局配置文件(mybatis-config.xml)和所有的 Mapper XML 文件(或注解)。
    • 它会将每一个 SQL 标签(如 <select id="selectUser">)解析成一个 MappedStatement 对象,并将其存储在一个巨大的 Configuration 对象中。MappedStatement 的 Key 通常是 namespace.id(例如 com.example.mapper.UserMapper.selectUser)。
  2. 代理阶段(获取 Mapper 时)
    • 当我们通过 sqlSession.getMapper(UserMapper.class) 获取 Mapper 接口的实例时,MyBatis 会使用 JDK 动态代理为该接口生成一个代理对象(MapperProxy)。
  3. 映射与执行阶段(调用方法时)
    • 当我们调用代理对象的方法时(如 userMapper.selectUser(1)),代理对象 MapperProxyinvoke 方法会被触发。
    • 关键步骤
      a. 定位 MappedStatement: 根据接口的全限定名 + 方法名 组合成 Key,从 Configuration 中找到对应的 MappedStatement
      b. 参数转换: 将传入的 Java 参数转换为 SQL 执行时所需的参数。
      c. SQL 执行: 将 MappedStatement、参数等信息传递给底层的 Executor 执行器。
      d. 结果映射Executor 通过 JDBC 执行 SQL 后,获取 ResultSet。然后根据 MappedStatement 中定义的结果映射规则(<resultMap> 或自动映射),将 ResultSet 的一行行数据转换为 Java 对象。

总结:MyBatis 通过 “接口全限定名+方法名”作为ID,在启动时建立起一个 SQL 命令的仓库(Configuration),运行时通过动态代理将接口方法调用映射到仓库中具体的 SQL 命令上,最后由执行器完成数据库操作和结果集到 Java 对象的转换。

二、SQL 映射与动态 SQL

6. #{} 和 ${} 的区别?哪个能防止 SQL 注入?

这是一个非常高频的面试题,考察点在于SQL注入安全和MyBatis的参数处理机制。

特性#{} (占位符)${} (拼接符)
处理方式预编译(PreparedStatement)字符串直接替换(Statement)
安全性能有效防止SQL注入不能防止SQL注入,存在安全风险
底层实现会被解析成 ?,然后使用 PreparedStatement.setXxx() 方法来安全地设置参数。会将 ${} 内的内容原样拼接在SQL语句中,然后编译执行。
数据库优化数据库可以对预编译的SQL进行缓存,有利于性能优化。每次都是一个新的SQL语句,无法利用预编译缓存。
适用场景传入参数的值(Where条件中的值,Insert的值等)。99%的场景都应使用#{}动态传入表名、列名等非值参数。例如: ORDER BY ${columnName}
示例SELECT * FROM user WHERE name = #{name} 解析为:SELECT * FROM user WHERE name = ?,参数 ‘Alice’ 会被安全地设置进去。SELECT * FROM ${tableName} WHERE name = ${name} 如果 tableNameusername‘Alice’,则解析为: SELECT * FROM user WHERE name = Alice(语法错误) 如果 name 被恶意传入 ‘Alice’ OR ‘1’=‘1’,则解析为: SELECT * FROM user WHERE name = Alice OR ‘1’=‘1’(查询出所有数据,SQL注入成功)。

结论

  • #{} 能防止SQL注入,应优先使用。
  • ${} 有SQL注入风险,除非是动态表名/列名等必须使用字符串替换的场景,否则严禁使用。

7. MyBatis 中如何处理字段名与属性名不一致?

当数据库字段名(如 user_name)和 Java 实体类属性名(如 userName)不一致时,查询结果无法自动映射。有三种解决方案:

1.起别名(最原始)
在 SQL 语句中为字段起一个与属性名相同的别名。

SELECT user_id as id, user_name as userName FROM user;

2.使用 resultMap(最强大、最常用)
定义一个 <resultMap> 来精确地指定数据库字段和Java属性的映射关系。这是最推荐的方式。

<resultMap id="UserResultMap" type="com.example.User"><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- 其他字段映射 -->
</resultMap><select id="selectUser" resultMap="UserResultMap">SELECT * FROM user
</select>

3.开启驼峰命名自动映射(最方便)
在 MyBatis 的全局配置文件中(如 application.ymlmybatis-config.xml)设置 mapUnderscoreToCamelCasetrue。MyBatis 会自动将下划线风格的字段名(user_name)转换为驼峰风格的属性名(userName)。

# application.yml
mybatis:configuration:map-underscore-to-camel-case: true

8. resultType 和 resultMap 的区别?

特性resultTyperesultMap
映射方式自动映射手动映射
灵活性低。要求查询返回的列名(或别名)必须与Java对象的属性名严格一致(或开启驼峰映射)。高。可以自定义复杂的映射关系,解决任何字段名和属性名不一致的问题。
功能只能简单映射。功能强大,可以处理: 1. 字段名/属性名不一致。 2. 复杂类型关联(一对一、一对多)。 3. 继承关系映射。
使用场景查询结果非常简单,且字段名与属性名完全对应。所有需要自定义映射规则的场景,尤其是有关联查询时必须使用

总结resultType 是自动挡,简单省事但受限;resultMap 是手动挡,操控精准功能强。

9. MyBatis 动态 SQL 有哪些标签?执行原理是?

动态SQL标签

  • <if>: 条件判断。
  • <choose>、<when>、<otherwise>: 类似 Java 中的 switch-case-default,实现多选一。
  • <trim>、<where>、<set>: 智能地处理SQL语句的前缀、后缀和多余的连接词(如AND, OR, 逗号)。
    • <where> 会智能地去掉开头多余的 AND/OR
    • <set> 会智能地去掉结尾多余的逗号。
  • <foreach>: 遍历集合,常用于 IN 条件或批量操作。
<!-- 示例:批量查询 -->
<select id="selectUsersByIds" resultType="User">SELECT * FROM user WHERE id IN<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
</select>

执行原理
MyBatis 的动态 SQL 功能是基于 OGNL (Object-Graph Navigation Language) 表达式和内部强大的 SqlNodeSqlSource 等组件实现的。

  1. 解析阶段: 在应用启动时,MyBatis 会解析 Mapper XML 文件。它会将动态 SQL 标签解析成一个个 SqlNode 对象,形成一个混合的静态/动态 SQL 树状结构
  2. 执行阶段: 当执行 SQL 时,MyBatis 会传入参数对象。
  3. 应用OGNL: MyBatis 会使用 OGNL 表达式来评估动态标签中的判断条件(如 <if test="name != null">)。
  4. 拼接SQL: 根据评估结果(true/false),SqlNode 会动态地拼接出最终的、完整的 SQL 字符串。<where>, <set> 等标签会在此过程中智能地处理语法问题。
  5. 参数设置: 最终,这个拼接好的 SQL 会被交给 PreparedStatement,并使用 #{} 占位符的方式安全地设置参数。

10. 如何实现一对多、多对一、多对多关联查询?

主要通过 <association><collection> 标签在 <resultMap> 中实现。

  • 多对一 / 一对一 (<association>)
    例如:多个订单属于一个用户(多对一)。
<resultMap id="OrderWithUserResultMap" type="Order"><!-- 映射Order本身的基本字段 --><id property="id" column="order_id"/><result property="orderNumber" column="order_number"/><!-- association 映射关联的单个User对象 --><association property="user" javaType="User"><id property="id" column="user_id"/><result property="userName" column="user_name"/></association>
</resultMap><select id="selectOrderWithUser" resultMap="OrderWithUserResultMap">SELECT o.order_id, o.order_number, u.user_id, u.user_nameFROM orders o LEFT JOIN user u ON o.user_id = u.user_idWHERE o.order_id = #{id}
</select>
  • 一对多 (<collection>)

    例如:一个用户有多个订单(一对多)。

<resultMap id="UserWithOrdersResultMap" type="User"><!-- 映射User本身的基本字段 --><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- collection 映射关联的Order集合 --><collection property="orders" ofType="Order"><id property="id" column="order_id"/><result property="orderNumber" column="order_number"/></collection>
</resultMap><select id="selectUserWithOrders" resultMap="UserWithOrdersResultMap">SELECT u.user_id, u.user_name, o.order_id, o.order_numberFROM user u LEFT JOIN orders o ON u.user_id = o.user_idWHERE u.user_id = #{id}
</select>
  • 多对多

    多对多关系需要借助中间表。例如:用户和角色。一个用户有多个角色,一个角色属于多个用户。查询用户及其角色列表,本质上仍然是一对多查询,只是关联关系通过中间表实现。

<resultMap id="UserWithRolesResultMap" type="User"><id property="id" column="user_id"/><result property="userName" column="user_name"/><!-- 角色集合 --><collection property="roles" ofType="Role"><id property="roleId" column="role_id"/><result property="roleName" column="role_name"/></collection>
</resultMap><select id="selectUserWithRoles" resultMap="UserWithRolesResultMap">SELECT u.user_id, u.user_name, r.role_id, r.role_nameFROM user uLEFT JOIN user_role ur ON u.user_id = ur.user_idLEFT JOIN role r ON ur.role_id = r.role_idWHERE u.user_id = #{id}
</select>

11. MyBatis 支持延迟加载吗?原理是?

支持。延迟加载(懒加载)是 MyBatis 提供的一种优化手段。

  • 什么是延迟加载?
    在关联查询(如查询用户及其订单)时,默认情况下(急加载),MyBatis 会通过一条复杂的联表SQL将主对象(用户)和关联对象(订单)的数据一次性全部查询出来。
    延迟加载的意思是:先查询主对象(用户)。只有当程序真正去访问主对象中的关联对象(如调用 user.getOrders() 方法)时,MyBatis 才会发起第二条SQL去查询关联的订单数据。
  • 如何开启?
    在全局配置中开启:
mybatis:configuration:lazy-loading-enabled: true       # 开启延迟加载aggressive-lazy-loading: false   # 禁用“侵略性”延迟加载(重要)
  • 原理是什么?
    MyBatis 的实现非常巧妙,它利用了 动态代理

    1. 当你执行查询主对象的SQL时(如 selectUserById),MyBatis 返回的 User 对象中,orders 属性并不是真正的 List<Order>,而是一个由 MyBatis 生成的、实现了 List<Order> 接口的代理对象
    2. 这个代理对象内部保存了能够单独查询订单数据的SQL语句和参数。
    3. 当你第一次调用 user.getOrders() 方法时,这个代理对象会被触发,它去执行之前保存的SQL,查询出真正的订单数据,然后返回给你。
  • 优点: 提高了性能,特别是在不需要使用关联数据的情况下,避免了执行复杂SQL和传输多余数据。

  • 缺点: 可能会遇到著名的 “N+1 查询问题”。如果先查询一个用户列表(1条SQL),然后遍历每个用户并访问其订单(N条SQL),就会产生大量数据库查询,反而降低性能。因此,延迟加载需要根据业务场景谨慎使用。

三、缓存机制

12. MyBatis 的一级缓存和二级缓存区别?

这道题要求对MyBatis的两级缓存有全局的理解。以下是核心区别的对比:

特性一级缓存 (Local Cache)二级缓存 (Second Level Cache)
作用范围SqlSession 级别Mapper (Namespace) 级别
生命周期SqlSession 相同。SqlSession 关闭或提交,缓存就清空。与整个应用(SqlSessionFactory)相同。多个 SqlSession 可共享。
是否默认开启,且无法关闭。,需要手动配置开启。
存储位置内存(SqlSession 对象内部)。内存(默认,可配置到Redis等外部存储)。
如何开启自动开启。在Mapper.xml中添加 <cache/> 标签或在接口上加 @CacheNamespace
共享性不能共享。每个 SqlSession 有自己独立的一级缓存。可以共享。同一个 Mapper 下的所有 SqlSession 操作都会影响同一个二级缓存。
执行顺序二级缓存 -> 一级缓存 -> 数据库。查询时先看二级缓存,再看一级缓存。

一个形象的比喻

  • 一级缓存 像是你的个人工作台。你刚查过的资料放在手边,自己随时可以快速拿到,但别人看不到,你下班了(关闭Session)资料就被清走了。
  • 二级缓存 像是团队的共享资料库。任何人(任何SqlSession)查过的资料都会放一份进去,大家都可以来取,资料会长期保存(直到根据策略被清理)。

13. 一级缓存什么时候会失效?

一级缓存失效(清空)的时机非常重要,是保证数据一致性的关键。以下操作会导致一级缓存失效:

1.执行 commit() 操作
提交事务时,MyBatis会清空一级缓存,以避免后续查询读到脏数据。这是最常见的原因。

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user1 = mapper.selectById(1); // 查询数据库,放入缓存
user1.setName("New Name");
mapper.updateUser(user1); // 执行更新,但缓存还未清空
// sqlSession.commit(); // 一旦执行commit,一级缓存被清空
User user2 = mapper.selectById(1); // 如果未commit,从缓存取(脏读);如果commit了,重新查询数据库。

2.执行 rollback() 操作
回滚事务时,同样会清空一级缓存。

3.执行任意 INSERTUPDATEDELETE 操作(即使不是修改的同一数据)
只要执行了增删改操作,无论是否提交,一级缓存都会立即被清空。这是一种保守但安全的策略。

4.手动清空缓存
调用 SqlSessionclearCache() 方法会手动清空其一级缓存。

5.关闭 SqlSession
SqlSession.close() 会清空其一级缓存。

关键点:一级缓存的生命周期非常短,尤其是在有写操作的场景下,它很容易失效。因此,不要过分依赖一级缓存能带来巨大的性能提升。

14. 如何开启二级缓存?二级缓存的清理策略?

如何开启二级缓存?

1.全局配置(确保开启): 在 mybatis-config.xml 中,<settings> 标签下的 cacheEnabled 设置为 true(默认就是 true,通常无需修改)。

<settings><setting name="cacheEnabled" value="true"/>
</settings>

2.在具体的Mapper中声明

  • XML方式: 在对应的 Mapper.xml 文件中添加 <cache/> 标签。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper"><!-- 开启本Mapper的二级缓存 --><cache/><!-- 其他SQL定义 -->
</mapper>
  • 注解方式: 在Mapper接口上使用 @CacheNamespace 注解。
@CacheNamespace
public interface UserMapper {// ...
}

3.实体类序列化: 使用二级缓存的POJO类必须实现 Serializable 接口。

二级缓存的清理策略(可配置性)

<cache/> 标签可以配置多个属性来控制缓存的行为(清理策略是核心):

<cacheeviction="LRU"flushInterval="60000"size="512"readOnly="true"/>
  • eviction(清除策略): 当缓存数量达到上限时,如何移除对象。
    • LRU最近最少使用,默认):移除最长时间不被使用的对象。
    • FIFO(先进先出):按对象进入缓存的顺序来移除它们。
    • SOFT(软引用):基于垃圾收集器状态和软引用规则移除对象。
    • WEAK(弱引用):更积极地基于垃圾收集器状态和弱引用规则移除对象。
  • flushInterval(刷新间隔): 缓存会自动刷新的时间间隔,单位毫秒。默认不清空,设置后到时间会清空缓存,避免数据长期不更新。
  • size(引用数目): 缓存最多可以存储的对象数量。
  • readOnly(只读): 如果为 true,缓存返回的是相同的对象实例,性能好但不安全。如果为 false,缓存会返回对象的拷贝(通过序列化),安全但性能稍差。

四、分页与性能优化

15. MyBatis 如何实现分页?(物理分页 vs 逻辑分页)

MyBatis 本身不提供内置的分页功能,但可以通过多种方式实现,主要分为逻辑分页物理分页

1. 逻辑分页 (Logical Pagination)
  • 实现方式: 使用 MyBatis 内置的 RowBounds 对象。
  • 原理在查询出所有数据之后,在内存中进行分页。
    • 执行 SQL 时,依然是 SELECT * FROM table,查询出所有符合条件的数据。
    • MyBatis 的 ResultSetHandler 在处理结果集时,会根据 RowBounds 指定的 offset(偏移量)和 limit(限制条数)在内存中手动“跳过”前面的记录,只返回指定范围的数据。
  • 代码示例
int offset = 10; // 起始行,相当于 (pageNum - 1) * pageSize
int limit = 5;   // 每页条数
RowBounds rowBounds = new RowBounds(offset, limit);List<User> users = sqlSession.selectList("com.example.mapper.UserMapper.selectAllUsers", null, rowBounds);
  • 优点: 对于少量数据简单方便。
  • 缺点性能极差! 如果数据量巨大(如100万条),会先将这100万条数据从数据库全部加载到应用服务器内存中,再进行分页,极易导致内存溢出(OOM)。严禁在生产环境中用于大数据量分页。
2. 物理分页 (Physical Pagination)
  • 原理在数据库层面进行分页,SQL 语句中直接使用数据库特有的分页关键字(如 LIMIT, ROWNUM),只查询当前页需要的数据。
  • 实现方式
    • 手动编写分页SQL(推荐): 最直接、性能最好的方式。
-- MySQL
SELECT * FROM user ORDER BY id LIMIT #{offset}, #{pageSize}-- Oracle(使用ROWNUM,较复杂)
SELECT * FROM (SELECT a.*, ROWNUM rn FROM (SELECT * FROM user ORDER BY id) a WHERE ROWNUM <= #{end}
) WHERE rn > #{start}
  • 使用分页插件(最常用)**: 例如 PageHelper,它可以自动拦截SQL并为其加上分页语句,开发者无需关心不同数据库的语法差异。
  • 优点性能高,只传输和处理一页的数据,减轻了数据库和网络的负担。
  • 缺点: 需要编写复杂SQL或引入第三方插件。

结论生产环境中必须使用物理分页。

16. 分页插件(如 PageHelper)的原理?

以最流行的 PageHelper 为例,其核心原理是 MyBatis 插件(拦截器)机制

  1. 插件声明PageHelper 实现了 MyBatis 的 Interceptor 接口,并声明它要拦截的是 Executor 的查询方法。
  2. 拦截时机: 当你在调用DAO方法之前,使用 PageHelper.startPage(pageNum, pageSize) 方法后,它会将分页参数(页码、大小)存储在一个 ThreadLocal 变量中。
  3. 执行拦截
    • 当MyBatis执行SQL时,PageHelper 插件会拦截 Executorquery 方法。
    • 插件从 ThreadLocal 中获取到分页参数。
    • 插件利用数据库方言(Dialect)将原始SQL重写为带有分页功能的物理分页SQL。
      • 原始SQL: SELECT * FROM user
      • 重写后SQL(MySQL): SELECT * FROM user LIMIT 0, 10
  4. 查询总数(用于计算总页数):
    • 插件还会自动执行一条 COUNT(*) 查询来获取数据总数,例如: SELECT COUNT(*) FROM user
    • 它将总数和分页数据一起封装到一个 PageInfo 对象中返回给用户。
  5. 清理 ThreadLocal: 在分页查询结束后,插件会自动清除 ThreadLocal 中的分页参数,防止影响后续的非分页查询。

总结:PageHelper 通过动态代理和SQL重写技术,将开发者的逻辑分页请求,透明地、自动化地转换成了高效的物理分页。

17. 如何优化 MyBatis 的批量插入?

直接使用 foreach 标签在循环中多次执行单条INSERT语句效率很低,因为每次都要与数据库建立一次网络连接和事务开销。优化方案如下:

1. 使用 foreach 标签拼接批量SQL(推荐用于数据量不大时)

将多条INSERT语句合并成一条,利用SQL的批量插入语法。

<insert id="batchInsert">INSERT INTO user (name, email) VALUES<foreach collection="list" item="user" separator=",">(#{user.name}, #{user.email})</foreach>
</insert>
  • 生成SQLINSERT INTO user (name, email) VALUES (?, ?), (?, ?), (?, ?)
  • 优点: 一次网络传输,效率高。
  • 缺点
    • 当数据量非常大时(如数万条),拼接的SQL字符串会非常长,可能超出数据库允许的最大包限制。
    • 对数据库的压力是瞬间的。
2. 使用 BatchExecutor(真正意义上的批量操作)

通过将 SqlSession 的执行器类型设置为 ExecutorType.BATCH

// 获取批量操作的SqlSession
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {UserMapper mapper = sqlSession.getMapper(UserMapper.class);for (int i = 0; i < 10000; i++) {User user = new User(...);mapper.insertUser(user); // 这里只是将SQL预编译并加入批处理队列,并未执行// 每积累一定数量(如500条),手动刷入一次数据库,避免内存溢出if (i % 500 == 0) {sqlSession.flushStatements();}}// 最后提交事务,执行剩余的批处理操作sqlSession.commit();
}
  • 原理BatchExecutor 会将多条INSERT语句进行预编译,然后通过 addBatch() 方法将其加入批处理,最后通过 executeBatch() 一次性发送到数据库执行。
  • 优点: 性能极高,适合超大数据量插入。能有效利用JDBC的批处理能力,减少网络交互次数。
  • 缺点: 代码稍复杂,需要手动管理 flushcommit

结论: 中小批量数据用 foreach 简单高效;超大批量数据(数万以上)用 BatchExecutor 更稳定可靠。

18. 如何获取自动生成的主键值?(useGeneratedKeys)

当我们插入一条记录后,经常需要获取数据库自动生成的主键(如MySQL的AUTO_INCREMENT)。MyBatis提供了两种方式:

1. 使用 useGeneratedKeyskeyProperty(推荐)

这是最简洁的方式。

<insert id="insertUser" useGeneratedKeys="true" keyProperty="id">INSERT INTO user (name, email) VALUES (#{name}, #{email})
</insert>

注解方式

@Options(useGeneratedKeys = true, keyProperty = "id")
@Insert("INSERT INTO user (name, email) VALUES (#{name}, #{email})")
int insertUser(User user);
  • 工作原理: MyBatis 会使用 JDBC 的 Statement.getGeneratedKeys() 方法来获取数据库生成的主键。
  • 效果: 插入成功后,生成的主键值会自动回填到传入的 User 对象的 id 属性中。
User user = new User();
user.setName("Alice");
user.setEmail("alice@example.com");
mapper.insertUser(user); // 执行插入
System.out.println(user.getId()); // 此处id已被赋值,即为数据库生成的主键
2. 使用 <selectKey> 标签(适用于不支持自增主键的数据库,如Oracle)
<insert id="insertUser"><selectKey keyProperty="id" resultType="long" order="BEFORE">SELECT SEQ_USER.NEXTVAL FROM DUAL <!-- Oracle序列 --></selectKey>INSERT INTO user (id, name, email) VALUES (#{id}, #{name}, #{email})
</insert>
  • order="BEFORE" 表示在执行INSERT语句之前先执行 <selectKey> 中的查询来获取主键。

19. MyBatis 是否支持预编译?如何开启?

是的,MyBatis 完全支持预编译,并且这是它的默认行为,也是其防止SQL注入的核心机制。

  • 什么是预编译?
    预编译是指数据库驱动程序先将SQL语句模板(带 ? 占位符)发送给数据库进行编译和优化。之后执行时,只需要将参数传递给这个已编译好的模板,而无需再次编译整个SQL语句。
  • MyBatis 如何实现?
    MyBatis 在处理 #{} 占位符时,底层就是使用 JDBC 的 PreparedStatement 对象来实现预编译的。
    • 你的Mapper中的SQL: SELECT * FROM user WHERE id = #{id}
    • MyBatis 交给JDBC的SQL: SELECT * FROM user WHERE id = ?
    • JDBC 会先预编译这个带 ? 的SQL,然后调用 pstmt.setInt(1, 5) 来安全地设置参数。
  • 如何“开启”?
    你不需要做任何事来“开启”预编译,因为只要你使用 #{},MyBatis 默认就使用 PreparedStatement 所谓的“开启”实际上是确保你没有错误地使用 ${} 来拼接SQL,因为 ${} 会绕过预编译机制。
  • 验证预编译
    你可以在日志中看到类似以下的输出,这证明SQL是预编译的:
==>  Preparing: SELECT * FROM user WHERE id = ?   (SQL已被预编译)
==>  Parameters: 1(Integer)                       (参数被安全地设置进去)

总结:MyBatis通过 #{} 语法和底层的 PreparedStatement 自动实现了预编译,这是其安全性和性能的重要保障。开发者需要做的就是坚持使用 #{}

http://www.dtcms.com/a/409832.html

相关文章:

  • Docker(一)—— Docker入门到精通:从基础概念到容器管理
  • python(44) : docker compose基于基础镜像部署python服务
  • VMware+RockyLinux+ikuai+docker+cri-docker+k8s+calico BGP网络 自用 实践笔记(四)
  • 深入理解 Docker:从入门到实践
  • 实战排查:Java 解析 Excel 大型 导致内存溢出问题的完整解决过程
  • 【实录】使用 Verdaccio 从零搭建私有 npm 仓库(含完整步骤及避坑指南)
  • 物联网人体红外检测系统详解
  • 关于Unix Domain Socket的使用入门
  • 机器视觉系统中工业相机的常见类型及其特点、应用
  • RTT操作系统(4)
  • 基于卷积神经网络的 CIFAR-10 图像分类实验报告
  • 微服务项目->在线oj系统(Java-Spring)----[前端]
  • 做网站撘框架小米手机如何做游戏视频网站
  • 如何建自己网站做淘宝客黄骅港吧
  • 交叉口内CAV调度:轨迹优化与目标速度规划,助力智能交通无缝运行!
  • Navicat 技术指引 | KingbaseES 专用 AI 助手
  • 如何优化Android app耗电量
  • 面试复习题---Flutter 资深专家
  • 在 C# 中将邮件转换为 PDF | MSG 转 PDF | EML 转 PDF
  • 【LangChain4j+Redis】会话记忆功能实现
  • Android Handler的runWithScissors方法
  • 180课时吃透Go语言游戏后端开发3:Go语言中其他常用的数据类型
  • 在 Android 11 上实现 WiFi 热点并发支持(同时开启 STA + AP 模式)
  • 济南高新区网站建设wordpress举报插件
  • html 占位符
  • GPT-5 Codex正式上线 Azure AI Foundry(国际版)
  • C++设计模式之结构型模式:享元模式(Flyweight)
  • STM32 智能垃圾桶项目笔记(一):超声波模块(HC-SR04)原理与驱动实现
  • 全文 -- Vortex: Extending the RISC-V ISA for GPGPU and 3D-Graphics Research
  • 设计网站推荐理由公司网站备案电话