☰
MyBatis核心原理与实战指南:从JDBC到动态SQL、缓存及面试高频问题
2026/10/6 13:46:00 网站建设 项目流程

做Java后端这几年,如果让我选一个最“省心”的数据库访问层方案,我大概率会毫不犹豫说出MyBatis。尤其是Java配合MySQL这套组合拳,MyBatis在其中扮演的角色,就像一个经验丰富的老翻译官——你把SQL写在XML里,它帮你把参数传进去、把结果集装成对象,整个过程透明可控。今天这篇文章,我就从自己踩过的坑、优化的SQL、写过的Mapper出发,给你一份带个人心得的MyBatis快速入门指南。不管你是准备Java面试的应届生,还是刚接触Spring Boot + MyBatis的开发者,这篇文章都值得你先收藏再细读。

先说清楚这篇文章要解决什么问题:第一,帮你把MyBatis从“会用”提升到“懂原理”;第二,把MySQL和MyBatis搭配时最容易翻车的细节全部摆出来;第三,针对网上高频出现的mybatis面试题,给出兼顾深度和实操的回答思路。我会把之前项目中真实遇到的情况穿插进去,而不是给你背概念。

1. 为什么选MyBatis:从JDBC到MyBatis的演进逻辑

1.1 纯粹的JDBC时代,痛在哪里

很多教程上来就讲MyBatis怎么用,却没人说清楚为什么需要它。我自己刚学Java时,写的最多的就是JDBC代码:Class.forName注册驱动、DriverManager.getConnection拿连接、PreparedStatement预编译、ResultSet遍历结果……一个最简单的查询用户列表,代码能写几十行,而且每行都得小心处理异常和资源关闭。

当时的痛点非常明显:

  • 样板代码太多:每次都要重复获取连接、创建Statement、遍历ResultSet、关闭资源,这些代码跟业务逻辑完全无关,但你又不能省略。
  • SQL与业务逻辑耦合:SQL字符串直接在Java代码里拼接,一旦SQL有变动,就要改Java代码重新编译,维护成本极高。
  • 参数与结果集映射靠手工:从ResultSet里getString、getInt,再一个个set到对象里,字段少还好,字段一多就痛苦不堪。
  • 性能隐患:手动拼接SQL容易引发SQL注入,而且连接管理不当会导致连接泄漏。

后来出现了Hibernate这样的全自动ORM框架,它确实能通过对象关系映射自动生成SQL,但全自动也意味着你失去了对SQL的完全控制。一旦遇到复杂的多表联查、分页优化、存储过程调用,Hibernate反而会变得笨重。这时候MyBatis出现了——它把SQL还给你,把映射工作接管过来,这正是“半自动ORM”的核心哲学。

1.2 MyBatis的核心设计思路:SQL与Java代码的优雅解耦

MyBatis的设计其实可以用一句话概括:SQL写在配置里,逻辑写在Java里,映射由框架完成。你不再需要在Java代码里拼接SQL,而是把SQL语句放到Mapper XML文件或者注解中,通过动态参数传入值,通过结果映射自动生成对象。

这种设计带来的直接好处有三个:

  • SQL的可维护性大幅提升,DBA可以直接看XML里的SQL做优化,而不需要翻Java代码。
  • 参数传递走的是PreparedStatement占位符机制,天然免疫SQL注入。
  • 结果映射支持驼峰命名自动转换、自定义resultMap,灵活度非常高。

我最近重构一个老项目时,就因为SQL集中在XML里,直接在配置文件里优化了两条慢查询SQL,改动只影响XML文件,Java代码一行没动,这种体验是JDBC时代完全不敢想的。

1.3 什么场景适合用MyBatis,什么场景该慎重

MyBatis不是银弹,选型时要想清楚。

适合用MyBatis的场景:

  • 项目需要精细控制SQL,比如报表查询、统计SQL、复杂联表。
  • 数据库是MySQL且表结构经常调整,SQL可以灵活变动。
  • 团队里Java开发对SQL比较熟悉,希望SQL可读、可控。
  • 老项目改造,原先就是JDBC或DBUtils,迁移成本低。

不建议硬上MyBatis的场景:

  • 业务对象模型非常复杂,且实体关系紧密,期望完全自动化的CRUD,可能JPA/Hibernate更省心。
  • 项目极度简单,只有几个表的增删改查,直接用Spring JDBC Template就够了。
  • 团队没有专门的SQL能力,写复杂SQL容易翻车。

本质上,选MyBatis就是选“把SQL控制权留在自己手里”。我自己在数据中台项目中就特别依赖这一点,因为每条统计SQL都要人工优化,MyBatis给了我最大的自由度。

2. 环境准备与快速上手:把项目跑起来

2.1 MySQL安装与初始化:Windows和Linux环境下的实操记录

很多新手卡在MySQL安装上,还没见到MyBatis就被劝退了。我结合自己的经验,把两个主流平台的安装要点说清楚。

Windows 10/11下安装MySQL 8.0:

  1. 从官网下载MySQL Installer,选择Server only版本即可。
  2. 安装时选“Developer Default”也没问题,但没必要装一堆组件,我用的是Custom,只勾选MySQL Server。
  3. 配置类型选“Development Computer”,端口默认3306。
  4. 设置root密码时,建议单独建一个普通用户供应用连接,不要所有服务都用root,这样能减少误操作风险。
  5. 安装完成后,在系统环境变量Path里添加MySQL的bin目录,比如C:\Program Files\MySQL\MySQL Server 8.0\bin,这样就能直接用mysql -u root -p命令。

我踩过的一个坑:Windows下如果端口被占用,安装后服务会启动失败。解决办法是在my.ini中改端口,或者用net stop mysql和net start mysql重启服务。另外,MySQL 8.0的默认认证插件是caching_sha2_password,有些旧版客户端连接会报错,可以在命令行执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';解决。

Linux下安装MySQL 8.0(以CentOS/RHEL为例):

wget https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm rpm -ivh mysql80-community-release-el7-3.noarch.rpm yum install mysql-community-server systemctl start mysqld grep 'temporary password' /var/log/mysqld.log

首次启动后MySQL会生成临时密码,记得先登录修改密码。如果docker安装MySQL失败,多半是镜像源问题,建议先docker pull mysql:8.0指定版本,避免拉取latest时出现平台不兼容。

对于产线环境,我强烈建议在安装完MySQL后顺手执行mysql_secure_installation,把匿名用户、测试库这些风险项全部关掉。安全是底线。

2.2 Maven依赖配置与MyBatis核心配置文件

项目用Maven管理,依赖很简单:

<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

如果用的是Spring Boot,直接引入mybatis-spring-boot-starter:

<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency>

MyBatis的核心配置文件mybatis-config.xml里,我最常配置的就是这几项:

<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> <typeAliases> <package name="com.example.entity"/> </typeAliases> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/test_db?useUnicode=true&amp;characterEncoding=utf8&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="123456"/> </dataSource> </environment> </environments> <mappers> <package name="com.example.mapper"/> </mappers> </configuration>

这里面的mapUnderscoreToCamelCase非常关键。MySQL字段习惯用下划线,比如user_name,Java属性用驼峰userName,开启这个设置后MyBatis会自动映射,省去写大量resultMap的麻烦。另外logImpl设为STDOUT_LOGGING,就是热词里提到的mybatis配置打印,开发阶段能看到完整SQL,排查问题极其方便。

2.3 第一个Mapper:从接口到XML的完整链路

我习惯把Mapper写成接口+XML的方式,因为SQL复杂时XML更清晰。三步走:

第一步,写实体类User:

public class User { private Long id; private String userName; private String password; // getter/setter 省略 }

第二步,写Mapper接口UserMapper:

public interface UserMapper { User selectById(@Param("id") Long id); List<User> selectAll(); int insert(User user); }

第三步,在resources/mapper/UserMapper.xml写SQL:

<?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"> <select id="selectById" resultType="com.example.entity.User"> SELECT id, user_name, password FROM t_user WHERE id = #{id} </select> <select id="selectAll" resultType="com.example.entity.User"> SELECT id, user_name, password FROM t_user </select> <insert id="insert" parameterType="com.example.entity.User" useGeneratedKeys="true" keyProperty="id"> INSERT INTO t_user(user_name, password) VALUES(#{userName}, #{password}) </insert> </mapper>

这里两个细节:namespace必须与Mapper接口全限定名一致;select的id必须与接口方法名一致。否则运行时会报Invalid bound statement (not found),这是新手最容易碰到的错。

3. 核心细节解析:参数传递、结果映射与动态SQL

3.1 参数传递的几种方式与#{}和${}的区别

MyBatis参数传递有好几种玩法,关键要搞清楚#{}和${}的区别,这是面试和实际开发中的高频点。

#{}是预编译占位符。MyBatis会把#{id}解析成?,然后通过PreparedStatement的setObject传入参数,这个过程能有效防止SQL注入。比如WHERE id = #{id},实际执行的是WHERE id = ?,用户传什么都被当成字符串值处理。

${}是字符串直接替换。MyBatis会直接把参数值拼接到SQL语句中,比如ORDER BY ${column},如果column写的是create_time,SQL就变成ORDER BY create_time。这种方式适合传入表名、列名、排序字段等#{}无法处理的结构性内容,但存在SQL注入风险,必须自己校验参数白名单。

参数传递的几种方式:

  • 单参数:selectById(Long id),XML里直接用#{id}即可。
  • 多参数:接口方法写selectByNameAndAge(@Param("name") String name, @Param("age") Integer age),XML里用#{name}和#{age}。
  • POJO对象:接口方法写insert(User user),XML里直接用#{userName}对应实体属性。
  • Map:接口方法写selectByMap(Map<String, Object> map),XML里用#{key}对应map的key。

我一般在多参数场景下强制@Param命名,因为如果不用@Param,MyBatis会把参数命名为param1、param2,XML里一旦写错字段很难排查。

3.2 resultType vs resultMap:复杂映射怎么处理

resultType是简化写法,MyBatis会自动把查询结果的列名映射到Java对象的同名字段上。如果开启了驼峰转换,user_name就能自动映射到userName。适用于字段名和属性名基本一致的情况。

resultMap则用于定制映射规则,适合以下场景:

  • 数据库字段和Java属性名完全对不上。
  • 需要联表查询,把关联对象映射到嵌套对象里。
  • 需要处理<association>和<collection>这样的复杂关系。

举个例子,查询用户及其订单列表:

<resultMap id="UserOrderMap" type="com.example.entity.User"> <id property="id" column="id"/> <result property="userName" column="user_name"/> <collection property="orders" ofType="com.example.entity.Order"> <id property="orderId" column="order_id"/> <result property="orderNo" column="order_no"/> </collection> </resultMap>

注意<collection>里的ofType是集合元素的类型,不是集合本身的类型。很多新手在这里写成java.util.List,结果映射出来全是null。

3.3 动态SQL:if/where/foreach实战

动态SQL是MyBatis的灵魂。所谓动态SQL,就是根据条件动态拼接SQL片段,解决了Java代码里“if判断拼字符串”的痛点。

最经典的就是多条件查询:

<select id="selectByCondition" resultType="com.example.entity.User"> SELECT * FROM t_user <where> <if test="userName != null and userName != ''"> AND user_name LIKE CONCAT('%', #{userName}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> </select>

<where>标签的作用是:如果条件成立,自动在SQL前面补上WHERE关键字,同时去掉第一个AND。这样就不用担心“WHERE后面直接跟AND”的语法错误。

批量插入场景下,<foreach>是必须的:

<insert id="batchInsert"> INSERT INTO t_user(user_name, password) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.password}) </foreach> </insert>

foreach的collection属性,单参List时写list,单参数组时写array,如果是@Param命名就写对应的名字。separator是元素之间的分隔符,这里用逗号就能拼出合法的多行INSERT语句。我在批量导入用户数据时,一次插入五万条,用这个方式比单条insert快了上百倍。

动态SQL里还有一个容易忽略的点:<script>标签。如果你不想写XML,想用注解写动态SQL,就得把SQL包在<script>里,例如@Select("<script>SELECT * FROM t_user WHERE id IN <foreach collection='ids' item='id' open='(' separator=',' close=')'>#{id}</foreach></script>")。但维护起来比较难受,我建议复杂SQL还是别用注解,老老实实写XML,代码可读性强,IDE还能帮你校验XML格式。

3.4 开发环境必备:MyBatis打印SQL配置

开发阶段把SQL打印出来,能省一半查错的时间。热词里的“mybatis配置打印”指的就是这个。在Spring Boot中配置很简单:

mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

配合logging.level.com.example.mapper: debug也能实现,更推荐用后者,因为它只打印指定包的SQL日志,不会刷屏:

logging: level: com.example.mapper: debug

打印日志时,你会在控制台看到类似这样的输出:

==> Preparing: SELECT * FROM t_user WHERE id = ? ==> Parameters: 123(Long) <== Columns: id, user_name, password <== Row: 123, zhangsan, 123456

这里能直观看到预编译的SQL和实际传入的参数,排查“SQL执行结果和预期不一致”时简直神器。生产环境记得把日志级别调到warn或error,避免敏感SQL信息刷屏。

4. 缓存机制:一级缓存与二级缓存

4.1 一级缓存的作用域与隐藏的坑

缓存是MyBatis面试绕不开的话题。先说一级缓存,它是SqlSession级别的缓存,默认开启,而且你关不掉。只要同一个SqlSession执行两次相同的SQL查询,第二次就会直接命中缓存返回结果,不再查询数据库。

一级缓存听起来很美好,但实际开发中容易踩坑:Spring整合MyBatis后,每个SqlSession的生命周期通常被管理得很短,你的一次Mapper方法调用可能就对应一个SqlSession,方法结束后SqlSession关闭,一级缓存立刻失效。所以Spring Boot中使用MyBatis时,一级缓存带来的性能提升其实非常有限。

更坑的是,如果同一个SqlSession里先查询某条数据,然后执行了insert、update、delete操作,MyBatis会清空一级缓存。因为数据变更后,之前的缓存数据可能过期了。这个设计是合理的,但如果你在同一个事务里,先查询、再修改、再查询,要多留意中间是否有脏数据产生。

还有一类坑和<select>标签的flushCache属性有关。默认情况下,select的flushCache是false,也就是不会清缓存。但如果你对于某个查询希望每次都是最新数据,可以设置flushCache="true",这样每次执行都会清空一级缓存并重新查询。

4.2 二级缓存:配置、实现要点与失效场景

二级缓存是Mapper级别的缓存,跨SqlSession共享,默认不开启。开启方式是在Mapper XML中加入:

<cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/>

几个参数的简单解释:

  • eviction:缓存的淘汰策略,LRU表示最近最少使用,还有FIFO、SOFT、WEAK。
  • flushInterval:缓存刷新间隔,单位毫秒。
  • size:缓存最多能存储的对象数量。
  • readOnly:设为true时,所有使用方拿到的都是同一份缓存对象,性能好但无法修改;设为false时,MyBatis会返回对象的序列化副本,更安全。

要想二级缓存生效,涉及到的实体类必须实现Serializable接口。因为MyBatis在readOnly=false时,会通过序列化和反序列化来复制对象。很多初学者漏掉这一步,结果发现二级缓存完全不生效。

二级缓存生效的前提是:同一个namespace(也就是同一个Mapper)下,所有查询才会共享缓存。如果你有两个Mapper查同一张表,但它们不在同一个namespace,二级缓存也不会互通。这一点在跨Mapper联查时特别容易误解。

另外,二级缓存在执行DML语句(insert/update/delete)时会自动清空整个namespace的缓存。所以对于写操作频繁的表,开二级缓存反而会因为频繁清空而效果变差,有时还会因为缓存清理不及时出现短暂的数据不一致。我通常在配置中心这种读多写少的场景下才开启二级缓存,普通业务表慎用。

4.3 缓存实战经验谈

我对MyBatis缓存的态度,一句话:别指望MyBatis缓存解决性能瓶颈,特别是互联网业务场景。一级缓存生命周期太短,二级缓存又存在多Mapper数据一致性问题。真要提高查询性能,我更推荐从这几方面下手:

  • SQL优化:加索引、避免SELECT *、控制返回行数。
  • 数据库连接池调优:比如HikariCP的maximumPoolSize配置。
  • 引入外部缓存:Redis承担热点数据缓存,MyBatis的缓存可以关闭或仅保留一级。

如果你一定要用MyBatis自带的二级缓存,建议配合flushInterval定期刷新,并且设置readOnly=false,宁可牺牲一点性能也要保证数据安全。

5. 常见问题与排查技巧实录

5.1 SQL条件不生效:一个must know的排查思路

热词“mybatis条件不生效”是很多人遇到过的。我也被坑过,后来总结了一套排查路径。

最常见的场景:

<select id="selectList" resultType="..."> SELECT * FROM t_user WHERE status = 1 <if test="name != null and name != ''"> AND user_name = #{name} </if> </select>

如果传入name为空,MyBatis直接不拼这个条件,查询结果变成status=1的所有记录,这不算bug。真正的问题是,明明传入name有值,但条件没生效。

先确认几个点:

  • test里面的属性名是否和POJO属性名一致。注意是Java属性名,不是数据库列名。
  • 是否加了@Param注解,如果加了,test里能直接用参数名;如果没加,可能需要用param1这样的名字。
  • 看控制台打印的SQL,有没有拼上AND user_name = ?。如果SQL没拼上,那就是test判断为false;如果SQL拼上了但查不到数据,那就要查参数是否真的传进来了。

另外还要注意<if>和<where>的配合问题。如果SQL中已经有了WHERE关键字,那么<if>直接写在里面会导致条件多出一个AND,比如WHERE AND user_name = ?,这直接就是语法错误。这时候一定要用<where>把多个<if>包起来。

5.2 数据库字段为null:映射结果与插入参数的双重陷阱

另一个高频问题是,数据库字段为null时,查询出来的对象属性明明是null,看起来正常,但更新或插入时,MyBatis默认不会把null值写入数据库。

比如你用updateUser方法更新某个用户,如果userName为null,MyBatis生成的UPDATE语句默认会忽略null字段,这样数据库里原有的userName就不会被清空。这在某些业务逻辑下是好事,但如果你就是想清空某个字段,就必须显式指定。

解决办法有两种:

一种是在XML里使用<set>动态标签显式控制:

<update id="updateUser"> UPDATE t_user <set> <if test="userName != null">user_name = #{userName},</if> <if test="password != null">password = #{password},</if> </set> WHERE id = #{id} </update>

另一种是全局修改update策略,在mybatis-config.xml中设置:

<setting name="jdbcTypeForNull" value="NULL"/>

这个设置是处理参数为null时,告诉JDBC使用NULL类型,避免某些数据库驱动不认null类型的问题。但要注意,它并不能让MyBatis把null值拼进UPDATE语句里。要真正做到“null也更新”,只能在<update>中使用<set>加上<if test="xxx != null">的相反逻辑,比较麻烦。所以最好的方案是:业务上明确要清空字段时,单独写一个更新SQL,不受动态SQL默认忽略null的干扰。

5.3 MySQL版本差异:5.7与8.0对MyBatis的影响

很多公司还在用MySQL 5.7,而自己学习却装了8.0,两者差异会导致一些MyBatis使用上的问题。

首先是驱动类名。MySQL 5.7通常用com.mysql.jdbc.Driver,8.0已经改为com.mysql.cj.jdbc.Driver。老驱动连8.0会报错,所以直接统一用新驱动即可。

其次是连接URL中的时区设置。MySQL 8.0强制要求serverTimezone,如果不加会报“The server time zone value"Öйú±ê׼ʱ¼ä"is unrecognized”。建议写serverTimezone=Asia/Shanghai。

然后是排序规则的差异。MySQL 5.7默认utf8mb4排序规则是utf8mb4_general_ci,8.0默认是utf8mb4_0900_ai_ci。如果从5.7迁移到8.0,可能出现索引失效或排序结果不一样的坑。遇到ORDER BY结果和预期不一致时,先看字段的排序规则。

还有一个小细节:MySQL 8.0里,GROUP BY的隐式排序被移除了。以前5.7中GROUP BY可能默认按分组字段排序,8.0不再保证,所以如果SQL依赖隐式排序,必须显式加ORDER BY。我在升级数据库后,就遇到过报表数据顺序错乱的问题,排查半天发现是版本差异。

5.4 Spring Boot集成MyBatis的实操注意事项

Spring Boot + MyBatis是当前最主流的组合,入门时主要注意几点。

第一,启动类上要加@MapperScan("com.example.mapper"),这会让Spring扫描到Mapper接口并创建代理对象。如果不想用@MapperScan,也可以在每一个Mapper接口上加@Mapper注解,效果一样。

第二,application.yml里需要配置Mapper XML的位置:

mybatis: mapper-locations: classpath:mapper/*.xml

如果不配,Spring Boot默认只扫描classpath根目录下与Mapper接口同包的同名XML,一旦目录对不上就报“Invalid bound statement”。

第三,事务处理。Spring Boot中可以用@Transactional注解管理数据库事务,MyBatis会参与其中。注意事务只对RuntimeException和Error回滚,如果方法里捕获了异常却不抛出,事务是不会回滚的。这个坑我实测踩过,尤其在做批量数据校验时,catch住异常但没重抛,结果脏数据入库了。

第四,分页插件。项目里几乎都会用PageHelper或者MyBatis-Plus的分页插件。以PageHelper为例,它的原理是拦截器解析SQL并改写成分页SQL。用PageHelper最忌讳的就是在循环里调用分页查询,或者把PageHelper.startPage写在查询语句之后,这样分页会失效甚至影响其他查询。合理用法是紧跟Mapper方法之前调用。

6. 面试题角度的知识复盘与源码阅读建议

6.1 高频MyBatis面试题问答整理

结合目前java面试题中的高频内容,我把MyBatis相关的典型问题和你该怎么回答,简单梳理一下。

问:#{}和${}的区别?
答要点:预编译VS占位符和字符串替换;前者防注入,后者适合动态表名/排序;使用${}必须做白名单校验。

问:MyBatis的缓存机制?
答要点:一级缓存SqlSession级别,二级缓存Mapper级别;二级缓存需要实体类实现Serializable;缓存清理条件;实际项目中一般用Redis替代。

问:MyBatis如何实现分页?
答要点:逻辑分页用RowBounds;物理分页用PageHelper插件,原理是MyBatis拦截器对Executor执行SQL时,通过Dialect改写SQL;也可以手写limit参数。

问:MyBatis中接口和XML如何绑定?
答要点:namespace与接口全限定名一致;方法id对应XML的statement id;参数映射通过@Param或参数名;绑定失败报Invalid bound statement。

问:MyBatis中类型处理器有用过吗?
答要点:TypeHandler用于Java类型和JDBC类型的转换,比如枚举、JSON字符串;可以自定义TypeHandler,注册到配置或XML中,处理特殊类型映射。

回答以上问题的关键点在于:不能只背结论,要能结合场景说出“为什么”。面试官想听到的是你踩过坑、理解原理,而不是在背八股文。

6.2 从源码层面理解MyBatis的执行流程

如果想在深度上加分,建议读一遍MyBatis的核心源码。我自己精读过SqlSession、Executor、StatementHandler这几个核心类,大概流程是这样:

  1. 创建SqlSessionFactory(基于Configuration配置)。
  2. 通过SqlSession.getMapper(XXMapper.class)获取Mapper代理对象。
  3. 调用Mapper方法,代理对象把方法转发给SqlSession。
  4. SqlSession根据statementId从Configuration中拿到MappedStatement。
  5. Executor执行SQL,先经过CacheExecutor(如果开启二级缓存)再交给SimpleExecutor或ReuseExecutor。
  6. StatementHandler负责创建PreparedStatement、设置参数、执行查询。
  7. ResultSetHandler将结果集映射成对象。

源码阅读建议从Configuration类入手,它会加载所有Mapper XML和注解配置。然后看MapperProxy,理解为什么Mapper接口没有实现类也能被调用——这就是动态代理的魅力。最后看BaseExecutor,搞清楚一级缓存的查询顺序:先查一级缓存,没有则查数据库,然后写入一级缓存。

读完源码之后,你对“mybatis条件不生效”“缓存失效”这类问题的理解会彻底上到一个新高度。

6.3 性能优化与日常开发的好习惯

最后聊一点个人体会。MyBatis的性能优化,重点不在框架本身,而在SQL与使用习惯。

  • 不要用SELECT *,明确列出字段,减少网络传输和结果集映射成本。
  • 批量操作尽量用foreach,避免循环里单条执行SQL。
  • 连接池配置合理,HikariCP的默认配置基本够用,但maximumPoolSize不宜过大,否则数据库负担重。
  • 开启SQL日志,定期审查慢查询,配合MySQL的EXPLAIN分析执行计划。
  • 尽量减少事务包裹范围,事务里只放必要的数据库操作,避免长时间占用连接。

把SQL写规范,比依赖任何框架技巧都更实在。MyBatis给了你灵活控制SQL的能力,你要做的就是把SQL本身练好——这算是我这几年最深刻的感受。抽空把本文提到的小坑都在自己项目里试一遍,比看十篇教程都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询