资讯详情

Spring Boot 集成 MyBatis 核心配置与原理实战指南

📅 2026/9/24 19:20:57 | 华诺云谱 👁 阅读
Spring Boot 集成 MyBatis 核心配置与原理实战指南
说个实在的现在 Java 后端面试和日常开发里Spring MyBatis 基本就是标配组合。尤其 Spring Boot 出来以后MyBatis 的集成难度被大幅降低但很多人只是会“用”一旦遇到缓存失效、分页慢、嵌套查询 N1、日志看不到 SQL 这类问题就容易抓瞎。这篇文章我不打算只贴一段配置就完事而是把 Spring 中使用 MyBatis 的完整链路拆开讲清楚从选型思路、核心配置、Mapper 开发、缓存机制到分页插件原理和常见坑尽量让你看完之后不仅会写还能知道为什么这么写。这套内容适合谁刚把 Spring Boot 跑起来、准备接数据库的新手可以照着一步步做写过一阵子 MyBatis 但没深究过缓存和拦截器的同学也能在这里找到不少“原来如此”的瞬间就算你是老手最后那部分问题排查清单也值得扫一眼里面有不少我实际踩过的坑。1. 技术选型与整体设计思路1.1 为什么是 Spring MyBatis 而不是 JPA 或纯 JDBC先聊点宏观的。你用 Spring 开发操作数据库的方案大致有三条路纯 JDBC、Spring JDBC Template、JPA/Hibernate、MyBatis。纯 JDBC 的问题不是不能用而是样板代码太多。我早期写过一段 JDBC 查询光处理 ResultSet 的 getString、getInt、null 判断一个方法就写了七八十行而且每个表都得来一遍维护成本极高。Spring JDBC Template 能做一定封装但复杂查询时 SQL 拼接依旧痛苦。JPA/Hibernate 则走了另一条路用对象关系映射帮你自动生成 SQL简单 CRUD 很爽但一旦涉及多表联查、复杂动态条件、报表类 SQL要么写 JPQL要么用原生 SQL 混搭反而有种“戴着镣铐跳舞”的感觉。MyBatis 的核心思路是SQL 你自己写映射关系我来管。它不做全自动的 SQL 生成而是把 SQL 的控制权完全交给你再把结果集自动映射成 Java 对象。这种做法对国内大量“SQL 为王”的业务系统非常友好——性能可控、调优直观、团队里任何一个人打开 XML 都能看懂查询逻辑。在 Spring 生态里MyBatis 通过 mybatis-spring 适配器无缝融入事务管理配合 Spring Boot 的 starter 更是开箱即用。所以选型上我的判断是如果你的项目是重 SQL、重报表、多表关联复杂的业务系统MyBatis 是性价比极高的选择如果项目模型简单、CRUD 为主、希望少写 SQL那 JPA 也可以考虑。但既然标题是 Spring 中使用 MyBatis下面的内容就全按 MyBatis 来展开。1.2 包结构与分层设计建议很多初学者喜欢把 Mapper 接口和 XML 文件分开乱放结果不是扫描不到就是路径配错。实际上 MyBatis 对 Mapper 的位置非常宽容但你需要有一条清晰的约定。我个人推荐的结构是标准的 Maven 分模块或单模块分层controller层负责接收 HTTP 请求、参数校验、返回结果封装。service层负责业务逻辑、事务边界、调用多个 Mapper。mapper包存放 MyBatis 的 Mapper 接口。resources/mapper目录存放对应的 XML 文件。entity或 domain、po包放数据库表对应的实体类。dto、vo包放接口入参和出参对象。这里有个常见问题Mapper 接口在com.example.demo.mapperXML 文件在resources/mapper下怎么让 MyBatis 知道它们的对应关系两种方式一种是在application.yml里写mybatis.mapper-locations: classpath:mapper/*.xml另一种是约定 XML 的 namespace 与接口全限定名一致。我个人建议两种都做到接口和 XML 名字保持一致配置里显式声明 mapper-locations双保险。注意如果 XML 文件的 namespace 写错了或者接口方法 id 与 XML 中的 statement id 对不上启动时往往不会立刻报错而是在第一次调用这个方法时抛出BindingException。这类问题排查起来比较费劲所以写接口时一定要保持接口方法名、XML id、SQL 三者的严格对应。1.3 依赖选型mybatis-spring-boot-starter vs 原生 mybatis mybatis-spring现在 Spring Boot 项目接 MyBatis 有三类依赖可用org.mybatis.spring.boot:mybatis-spring-boot-starter官方 starter版本随 Spring Boot 版本走例如 Spring Boot 2.7 用 2.3.xSpring Boot 3.x 用 3.0.x。org.mybatis:mybatisorg.mybatis:mybatis-spring手动组合适合非 Spring Boot 的纯 Spring 项目。com.baomidou:mybatis-plus-boot-starterMyBatis 的增强版内置通用 CRUD、分页插件、条件构造器。它底层仍是 MyBatis但别提的是它默认会改变一些行为比如逻辑删除、自动填充选择之前要和团队对齐。如果只是单纯想用 MyBatis 而不是 MyBatis-Plus我建议直接用官方 starter不要手动引入 mybatis 和 mybatis-spring除非你对版本兼容性有严格管控需求。starter 的好处是自动帮你注册SqlSessionFactory、SqlSessionTemplate、MapperScannerConfigurer还会把application.yml中的mybatis.*配置自动绑定到MybatisProperties。省事且不容易出错。2. 环境搭建与核心配置实操2.1 基于 Spring Boot 快速集成我用 Spring Boot 3.2 mybatis-spring-boot-starter 3.0.3 演示一个最小可用工程。首先在pom.xml中加入依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果你的数据库是 PostgreSQL、Oracle 或国产数据库替换驱动即可。接 MySQL 8.x 时注意驱动类名是com.mysql.cj.jdbc.Driver而不是老的com.mysql.jdbc.Driver。Spring Boot 3.x 下如果使用默认的 HikariCP 连接池基本不用额外配置。然后是application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这里有几个参数需要解释一下。map-underscore-to-camel-case我建议必须打开它可以把数据库的user_name自动映射为实体的userName省去一大堆column和property的显式声明。type-aliases-package则让 XML 中写resultType时可以直接写实体类的简单名称不用每次写全限定名。log-impl配成 StdOutImpl 会把执行的 SQL 直接打印到控制台开发阶段非常有帮助。2.2 启动类与 Mapper 扫描配置Spring Boot 中对 Mapper 接口有两种扫描方式在启动类上加MapperScan(com.example.demo.mapper)这个注解会扫描指定包下所有接口把它们注册为 MyBatis 的 Mapper。在每个 Mapper 接口上单独加Mapper注解这种方式比较啰嗦但更显式。两种方式可以共存但通常显式声明MapperScan就足够了。需要注意的是如果 Mapper 接口没有被任何注解扫描到Spring 容器启动时不会报错但你在 Service 里注入时会直接启动失败报NoSuchBeanDefinitionException。SpringBootApplication MapperScan(com.example.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }如果你写的不是 Spring Boot 而是传统 Spring 项目需要在 XML 或 JavaConfig 中配置Configuration MapperScan(com.example.demo.mapper) public class MyBatisConfig { Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setMapperLocations(new PathMatchingResourcePatternResolver() .getResources(classpath:mapper/*.xml)); org.apache.ibatis.session.Configuration configuration new org.apache.ibatis.session.Configuration(); configuration.setMapUnderscoreToCamelCase(true); factoryBean.setConfiguration(configuration); return factoryBean.getObject(); } }这只是用于理解幕后逻辑。实际上 Spring Boot starter 帮我们做了大部分事情传统 Spring 集成才需要手动写这些。2.3 日志与 SQL 打印开发期必备配置排查问题第一步永远是看 SQL。MyBatis 打印 SQL 有三种常见方式自带日志实现、mybatis.configuration.log-impl、logging.level配置 Mapper 包路径。如果你使用StdOutImplSQL 和参数都会直接输出到控制台格式比较原始但够用。生产环境不建议开StdOutImpl因为日志都进了控制台难以统一收集和管理。我在实际项目中发现一个坑如果同时配置了mybatis.configuration.log-impl和logging.level.com.example.demo.mapper: debug可能会看到 SQL 打印两次或者日志级别混乱。原因是 MyBatis 内部会根据 log-impl 找一个日志实现而 Spring Boot 的日志体系也会拦截 mapper 的 debug 日志。解决办法是想清楚到底用哪一套如果项目有统一的日志框架logback/log4j2建议用logging.level方式不设置 log-impl让 MyBatis 自动发现底层日志框架如果只是本地调试直接 log-impl 配 StdOutImpl 最直观。3. Mapper 开发从注解到 XML 的完整实践3.1 注解 SQL vs XML SQL选择与边界MyBatis 支持在 Mapper 接口方法上加Select、Insert、Update、Delete注解直接写 SQL。这种方式对极简单的 CRUD 很方便代码文件少看着清爽。Mapper public interface UserMapper { Select(SELECT * FROM user WHERE id #{id}) User findById(Long id); Insert(INSERT INTO user(name, age) VALUES(#{name}, #{age})) Options(useGeneratedKeys true, keyProperty id) int insert(User user); }但一旦遇到动态 SQLif、foreach、choose注解里写字符串拼接会非常痛苦可读性极差。比如一个根据多个可选条件查询用户列表的 SQL用注解写Select(script ... /script)能写到你怀疑人生。所以我的建议是单表简单操作可以用注解涉及多条件动态查询、多表关联、复杂映射的必须用 XML。3.2 XML 映射文件的完整示例我们先看一个完整的用户表 CRUD XML 应该长什么样?xml version1.0 encodingUTF-8? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper resultMap idUserResultMap typecom.example.demo.entity.User id propertyid columnid/ result propertyuserName columnuser_name/ result propertyage columnage/ result propertycreateTime columncreate_time/ /resultMap select idfindById resultMapUserResultMap SELECT id, user_name, age, create_time FROM user WHERE id #{id} /select select idlistByCondition resultTypeUser SELECT id, user_name, age, create_time FROM user where if testuserName ! null and userName ! AND user_name LIKE CONCAT(%, #{userName}, %) /if if testage ! null AND age #{age} /if /where ORDER BY create_time DESC /select insert idinsert parameterTypeUser useGeneratedKeystrue keyPropertyid INSERT INTO user(user_name, age) VALUES(#{userName}, #{age}) /insert update idupdate parameterTypeUser UPDATE user set if testuserName ! nulluser_name #{userName},/if if testage ! nullage #{age},/if /set WHERE id #{id} /update delete iddeleteById DELETE FROM user WHERE id #{id} /delete /mapper这里有三个高频易错点必须提where会自动去除多余的 AND/OR所以if里写AND age #{age}是安全的不要自己去掉 AND。set会自动处理末尾逗号但如果你在 if 内部写逗号多个条件组合时要注意不要多出多余逗号。resultTypeUser依赖type-aliases-package配置如果没配就必须写全限定名com.example.demo.entity.User。3.3 参数传递的几种方式与陷阱Mapper 接口方法的参数传递是日常开发中经常踩坑的地方。最基本的规则是#{id}是预编译占位符MyBatis 会生成?占位符并用 PreparedStatement 传参可以防 SQL 注入。${column}是字符串拼接直接把值拼进 SQL无法防注入只适合排序字段、表名等不能使用占位符的场景。单参数时如果参数是基本类型使用#{任意名字}都能取到。多参数时直接用#{param1}、#{param2}不太优雅建议用Param(xxx)进行显式命名ListUser findByCondition(Param(userName) String userName, Param(age) Integer age);XML 里写成select idfindByCondition resultTypeUser SELECT * FROM user WHERE age #{age} if testuserName ! null AND user_name #{userName} /if /select为什么不建议直接用#{param1}、#{param2}因为一旦调整参数顺序或者增加参数XML 里的名称就和方法对不上了非常容易出隐藏 bug。用Param显式命名既保证了可读性也提升了重构时的安全性。另外当参数是对象时#{userName}会直接取对象属性不需要Param但如果你还要同时传其他参数那就必须加上Param并在 XML 中使用#{user.userName}来访问属性。3.4 动态 SQL 的核心if、choose、foreach、trim动态 SQL 是 MyBatis 最值钱的能力。很多新手把这一块当成“会在 XML 里写 if”就没问题了实际远远不够。if是最基础的判断用于动态拼接条件。注意test里写的是 OGNL 表达式基本类型判断 null 和空字符串即可但如果是 Integer 类型判断! 会出问题因为 Integer 和字符串比较会走字符串转换建议只判断! null。choose相当于 Java 的 switchchoose when testqueryType name AND user_name #{keyword} /when when testqueryType age AND age #{keyword} /when otherwise AND create_time #{startTime} /otherwise /chooseforeach用于 IN 查询要特别注意collection的取值。如果参数是List直接用list如果是数组用array如果是Param(ids) ListLong ids则用idsselect idfindByIds resultTypeUser SELECT * FROM user WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /selecttrim是最灵活的标签where和set本质上就是 trim 的预设形式。理解trim的prefix、suffixOverrides可以让你写出更自由的控制逻辑。比如where等价于trim prefixWHERE prefixOverridesAND |OR set等价于trim prefixSET suffixOverrides,。3.5 ResultMap 与复杂映射当数据库字段和实体属性不完全一致时resultType的自动映射可能不够用。resultMap可以显式声明列与属性的映射关系还可以处理关联映射association和集合映射collection。举个例子查询订单同时返回订单对应的用户resultMap idOrderWithUserMap typecom.example.demo.entity.Order id propertyid columnorder_id/ result propertyorderNo columnorder_no/ result propertytotalAmount columntotal_amount/ association propertyuser javaTypecom.example.demo.entity.User id propertyid columnuser_id/ result propertyuserName columnuser_name/ /association /resultMap select idfindOrderWithUser resultMapOrderWithUserMap SELECT o.id AS order_id, o.order_no, o.total_amount, u.id AS user_id, u.user_name FROM orders o LEFT JOIN user u ON o.user_id u.id WHERE o.id #{id} /select关联查询时要特别注意id和result的命名尤其是联表后出现同名字段时比如两张表都有 id一定要用别名区分否则映射结果会错乱。4. 进阶机制缓存、分页插件与拦截器原理4.1 MyBatis 一级缓存与二级缓存原理、局限与坑MyBatis 的缓存机制分两级。一级缓存是SqlSession级别的默认开启且无法关闭。它的作用范围是同一个 SqlSession 中两次相同的查询第一次查询结果会缓存在本地 Map 中第二次相同查询直接返回缓存。在 Spring 环境中每次请求通常会新建 SqlSession使用完即关闭所以一级缓存的命中范围很小但对同一个事务内的重复查询还是有明显效果。这里有个经典坑如果同一个 SqlSession 中先查询了一条记录然后对这条记录做了 update再查同一条记录一级缓存会失效吗答案是会。MyBatis 在执行 insert/update/delete 时会把 SqlSession 的缓存清空。但如果先查两次再更新这两次查询之间缓存生效后一次只是查缓存不会真正执行 SQL。这在调试时会产生“明明数据库已经变了为什么查出来还是旧数据”的错觉。二级缓存是 Mapper 级别的跨 SqlSession 共享默认关闭。需要三步开启在 MyBatis 全局配置里设置cache-enabled: true。在 XML 中添加cache/标签局部缓存会优先生效。实体类需要实现Serializable接口因为二级缓存可能会写入磁盘或需要序列化传输。二级缓存的失效时机同样是执行增删改时清空对应 Mapper 的缓存。但需要注意如果使用多表查询并且多个 Mapper 共享表数据二级缓存非常容易产生脏读。比如订单查询缓存了用户信息之后直接通过 UserMapper 更新用户名OrderMapper 的缓存并不知道依然返回旧名称。这是我在项目里不推荐新手随便开二级缓存的核心原因。我的建议是默认只开启一级缓存不用二级缓存。分布式环境下用 Redis 做业务缓存远比本地二级缓存可靠。4.2 分页插件为什么高效PageHelper 的工作原理分页插件是 MyBatis 生态里使用频率极高的组件。最常见的 PageHelper 用法是PageHelper.startPage(pageNum, pageSize); ListUser userList userMapper.selectList(); PageInfoUser pageInfo new PageInfo(userList);第二行执行时PageHelper 会通过 MyBatis 拦截器机制拦截即将执行的 SQL在 SQL 末尾追加LIMIT ?, ?或对应的数据库方言分页语句同时执行一条 count 查询获取总数。这就是我们常说的“物理分页”和内存分页有本质区别。内存分页是把所有数据查出来后在 Java 内存里截取一段数据量大时必然 OOMPageHelper 的物理分页是在数据库层面完成性能可控。这里有几个 PageHelper 的经典坑PageHelper.startPage必须紧接着 Mapper 方法调用如果中间隔着其他查询方法分页会作用到错误的 SQL 上。使用 MyBatis-Plus 时startPage可能与 MP 自带的分页插件冲突建议只引入一个分页方案。PageHelper 的 count 查询在某些复杂 SQL比如包含 group by、union上可能生成错误的 count SQL必要时可以手写 count 查询并设置countSql。4.3 MyBatis 拦截器原理与一个实际案例MyBatis 的拦截器是它最强大的扩展点之一。其原理是使用 JDK 动态代理对四大核心对象Executor、StatementHandler、ParameterHandler、ResultSetHandler进行代理拦截。我们常见的分页、数据权限、字段自动填充底层都是靠拦截器实现的。实现一个拦截器需要实现org.apache.ibatis.plugin.Interceptor接口并用Intercepts注解声明要拦截的方法签名。这里给一个实际例子拦截所有查询自动追加一个逻辑删除的条件。Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}) }) public class LogicDeleteInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement (MappedStatement) invocation.getArgs()[0]; BoundSql boundSql mappedStatement.getBoundSql(invocation.getArgs()[1]); String oldSql boundSql.getSql(); // 简单判断是否包含某张需要逻辑删除的表实际生产需要更严谨的规则 String newSql oldSql AND deleted 0; // 通过反射修改 BoundSql 中的 sql 字段 Field field BoundSql.class.getDeclaredField(sql); field.setAccessible(true); field.set(boundSql, newSql); return invocation.proceed(); } }注意拦截器代码直接修改了BoundSql的私有字段这种反射方式的实现依赖于 MyBatis 内部实现细节升级 MyBatis 版本时需要回归测试。另外拦截器注册时需要添加到 MyBatis 配置中Configuration public class MyBatisConfig { Bean public ConfigurationCustomizer configurationCustomizer() { return configuration - { configuration.addInterceptor(new LogicDeleteInterceptor()); }; } }虽然拦截器很强大但我的经验是能不用就不用。拦截器是隐式的、全局的出了问题非常难排查。比如说你写了一条 SQL到数据库执行时发现多了个条件第一反应肯定去查业务代码而不会想到是某个拦截器在作怪。所以除非是分页、数据权限这类必须全局处理的场景否则尽量把逻辑写在 SQL 中保持显式。5. 性能优化与工程实践建议5.1 N1 查询问题与批量操作优化先说 N1 问题。当查询一个订单列表然后循环遍历每个订单去查对应的用户就会产生“1 条主查询 N 条子查询”的 N1 情况。数据量小的时候看着没问题数据量一大数据库连接和查询耗时都会飙升。解决办法有三个层面使用关联查询一次性把用户信息 join 出来。使用IN批量查询先把订单查出来收集所有 userId再用WHERE id IN (...)查询用户最后在内存中做映射。使用 MyBatis 的collection嵌套结果映射但要注意避免结果集笛卡尔积。批量插入也是优化重点。MyBatis 的foreach批量 insert 是常用方案insert idbatchInsert INSERT INTO user(user_name, age) VALUES foreach collectionlist itemuser separator, (#{user.userName}, #{user.age}) /foreach /insert这里有一个 MySQL 的注意事项max_allowed_packet默认可能有 4MB 或 64MB 的限制。如果一次批量插入的行数过大比如一次性插入 10 万条拼接的 SQL 可能超过包大小限制导致执行失败。建议分批操作比如每批 1000~2000 条既能保证执行效率又不会触及数据库单包限制。5.2 慢 SQL 排查从数据库到 MyBatis 的完整链路关于“mybatis update 执行慢”这类问题我的排查路径如下先看数据库本身用EXPLAIN分析 SQL 是否走了索引有没有全表扫描。看连接池状态如果连接池等待时间过长说明数据库连接不够用或者连接被长时间占用。看 MyBatis 日志确认执行的具体 SQL 和参数有可能你想执行的是索引字段但实际传参类型与字段类型不匹配导致索引失效。看是否被拦截器影响有的项目里配置了全局拦截器导致 SQL 被修改后无法走索引。还有一个常见情况是更新操作执行慢UPDATE user SET age #{age} WHERE id #{id}在表数据量大且 id 是主键时理论上应该很快。但如果数据库的innodb_buffer_pool_size太小或者事务隔离级别设置过高、存在锁等待都会表现为“慢”。建议排查时先跑一次SHOW PROCESSLIST看当前是否有锁等待再用慢查询日志定位具体 SQL。5.3 工程化建议统一分页、统一审计字段、代码生成到了项目工程化层面有几点实践值得坚持。第一统一分页返回结构。不要各个接口自己返回PageInfo或List建议统一封装成PageResultT包含total、current、size、records等字段前端对接成本会大大降低。第二统一审计字段。比如create_time、update_time、deleted、create_by这几个字段如果每个 Mapper 都手动写一遍容易遗漏。可以通过 MyBatis-Plus 的自动填充或者自定义拦截器统一处理。如果没有引入 MP我建议在实体中定义统一的 BaseEntity在插入和更新 SQL 中手动维护至少保证一致性。第三代码生成器。用 MyBatis GeneratorMBG或者 MyBatis-Plus 的代码生成器把基础 CRUD 代码自动生成出来然后人工改。这是最省力的方案而且能保证命名风格一致。我见过太多团队手写重复的UserMapper、UserServiceImpl耗时且容易出错。自动生成后再通过 Code Review 补充业务逻辑整体效率会高很多。6. 常见问题与排查技巧实录我把这些年实际遇到的 MyBatis 相关高频问题整理成一张速查表方便你直接对照处理。问题现象可能原因排查与解决方案启动报Invalid bound statement (not found)Mapper 接口与 XML 没有绑定检查 XML namespace 是否等于接口全限定名检查 mapper-locations 是否覆盖 XML 路径检查接口方法名与 XML id 是否一致查询返回 null没有任何 SQL 报错字段映射失败检查是否开启 map-underscore-to-camel-case检查 resultMap 中 column 与 property 是否对应检查 SQL 别名是否写错分页不生效返回全量数据PageHelper.startPage 与 Mapper 方法之间隔了其他查询确保 startPage 之后紧跟目标 Mapper 方法检查分页插件是否被重复注册日志看不到 SQL日志配置与 MyBatis log-impl 冲突确认 log-impl 是否设置确认 logging.level 是否覆盖 mapper 包为 debug检查是否被生产日志级别过滤Update执行很慢锁等待、索引失效、连接池不足用 SHOW PROCESSLIST 看锁等待用 EXPLAIN 分析索引检查连接池活跃数检查 SQL 参数类型是否匹配更新后查询结果还是旧数据一级或二级缓存未失效确认是否同一个 SqlSession 中先查询后更新再查询检查是否开启二级缓存且未清空对应 Mapper 缓存foreach 批量插入报packet too large单次插入条数过多超过 max_allowed_packet分批插入每批 1000~2000 条增大 max_allowed_packet不推荐无限调大多个数据源时 Mapper 注入错乱MapperScan 扫描到了多个数据源对应的 Mapper每个数据源使用独立的 MapperScan配合 basePackages 与 SqlSessionTemplate 区分接口返回的 List 是空但不是 null前端判断出错MyBatis 查询结果为 null 时List 类型字段返回空集合在 Service 层统一判断或者前端统一使用 isEmpty 判断6.1 一次“日志看不到 SQL”的实际排查过程有一次我帮同事排查一个接口数据库里数据明明存在但接口返回空。打开控制台想看看 SQL结果发现啥日志都没有。我当时的第一反应不是去改代码而是先看他的application.yml。果然他的log-impl没有配置而且logging.level.com.example.demo.mapper也没有设置。于是 MyBatis 走了 Spring Boot 默认的日志体系但 mapper 包默认级别是 infoSQL 的 debug 日志当然不会输出。加上一行配置就解决了logging: level: com.example.demo.mapper: debug这类问题不复杂但如果你不知道 MyBatis 日志的层级脉络就会无从下手。6.2 一个典型的 N1 优化案例我再分享一个真实优化案例。某个订单列表接口每条订单需要查用户名称、商品名称、店铺名称最初代码是在循环里分别调用 UserMapper、ProductMapper、ShopMapper。一次查询 100 条订单就会执行 1 100 100 100 301 条 SQL耗时 800ms 左右。优化方案是先把订单列表查出来然后收集所有 userId、productId、shopId分别用IN查询一次得到用户/商品/店铺的 Map在内存中组装。优化后 SQL 总数变成 4 条耗时降到 50ms。这个案例说明MyBatis 的 SQL 能力再强也救不了循环查询这种应用层问题。批量查询 内存组装往往是最稳妥的优化手段。6.3 缓存导致的数据“不改”假象还有一个非常有代表性的缓存踩坑经历。一个管理后台系统运营反馈说“用户信息改完再查还是旧数据”。我们查了数据库数据明明已经更新了。最后定位到问题出在二级缓存OrderMapper 的一个查询结果被二级缓存缓存了而 Order 表里关联了 User 表的信息。更新 User 表时只清空了 UserMapper 的二级缓存OrderMapper 的缓存没有失效所以通过订单维度查用户信息时命中旧缓存。最终方案是把 OrderMapper 的cache/标签去掉同时业务层引入 Redis 缓存并设置合理的过期时间。这个案例也是我后来坚持“默认不用二级缓存”的原因。7. 后续可以玩的扩展方向如果你把上面这些内容都吃透了Spring MyBatis 这块基本就没什么能难住你的了。接下来可以按兴趣继续深入阅读 MyBatis 源码中SqlSessionTemplate与MapperProxy的逻辑理解为什么 Mapper 接口不需要实现类也能被注入。尝试用 MyBatis 拦截器实现一个简单的数据权限插件给部门、租户级别的数据隔离练手。引入 MyBatis-Plus对比它与原生 MyBatis 在通用 CRUD、多租户、乐观锁、逻辑删除上的异同。结合 Spring Boot 的Transactional看 MyBatis 的 SqlSession 和数据库连接的绑定关系深入理解事务与连接池的协作原理。我个人在实际使用中有个体会MyBatis 的上手门槛确实低但真正拉开水平差距的往往是你对 SQL 本身的理解以及对 MyBatis 内部机制缓存、拦截器、动态代理的把握程度。工具只是中间层底层还是数据库和 SQL 基本功。希望这篇文章能帮你在 Spring MyBatis 这条路上少踩几个坑遇到问题时能多一份把握。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。