ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MyBatis核心机制与实战:动态SQL、缓存、分页及Spring Boot集成

MyBatis核心机制与实战:动态SQL、缓存、分页及Spring Boot集成 1. 内容整体设计与思路拆解提到MyBatis很多Java开发者的第一反应是半自动ORM框架。确实相比Hibernate的全自动映射MyBatis更像是把SQL掌握在开发者自己手里的工具箱。它对SQL并不陌生也没有试图把一切封装成黑盒而是让开发者写SQL、定义映射规则框架帮忙处理JDBC繁琐的代码和参数映射。正因为这种把控制权交给开发者的同时省去重复代码的设计它在国内企业级项目中认可度一直很高尤其配合Spring Boot之后几乎成了中小型项目数据访问层的标配。我这些年用MyBatis做过电商后台、库存系统、报表查询也维护过老项目里几千行的XML映射文件。对这个框架最大的体会是入门很容易但想用好必须在几个关键点上想清楚。比如参数传递、结果映射、动态SQL、缓存机制还有跟Spring Boot整合时的坑。很多人知道怎么用select和insert但一遇到复杂嵌套查询、缓存失效、性能问题就抓瞎。这篇博文就是想把MyBatis里常被低估、但又直接影响项目质量的部分讲透结合我在实际项目里的落地经验从设计思路到源码级别一步步拆开。内容适合正在学习SSM或Spring BootMyBatis的初学者也适合已经用了一段时间、想深入理解框架原理、提升数据访问层代码质量的开发者。无论你是准备面试还是想优化Oracle优化都能从这几年的内容中找到能直接抄作业的方法。先说我对MyBatis整体设计的一个看法它最大的价值不是让SQL执行变快而是让SQL执行代码的维护成本变低。JDBC原生写法里每个查询都要反复写Connection、PreparedStatement、ResultSet的处理业务代码被样板代码淹没。MyBatis把这段提取成了可复用的框架逻辑同时保留SQL的灵活性。代价就是开发人员需要在配置上多花心思XML映射和注解之间、参数绑定和结果集之间都有不少规则。理解了这一点你就明白为什么我在团队里一直强调MyBatis项目里真正需要精心设计的是SQL和Mapper接口的契约而不是依赖框架做过多自动猜测。框架只负责把接口方法和SQL绑定、把参数传进去、把结果映射出来至于SQL怎么写依然是你的核心工作。2. 核心功能与基本原理拆解2.1 从JDBC到MyBatis解决了什么问题先回顾一下最原始的JDBC写法便于理解MyBatis的工作内容。一段简单的按用户ID查询需要手动获取连接、预编译语句、设置参数、执行查询、遍历结果集、处理异常并释放资源。如果表字段多、查询条件多这段代码会非常冗长而且异常处理稍有疏漏就会导致连接泄漏。MyBatis做的事情可以概括成五层用SqlSessionFactory管理数据库连接和全局配置替代手工获取连接的重复代码。通过Mapper接口动态代理把接口方法映射到XML或注解中的SQL。参数处理统一化将#{}预编译参数自动转为JDBC占位符将${}直接拼接到SQL但需要自己防注入。结果映射统一化通过resultMap完成从数据库字段到Java属性包括嵌套对象、集合的映射。提供插件机制可以拦截Executor、StatementHandler等核心组件实现分页、性能监控等功能。举个例子你觉得一个老手写JDBC五分钟能完成的查询如果用MyBatis只要三分钟。重要的是查询上千行的复杂多表关联SQLMyBatis的XML方式比Java代码拼接SQL好维护太多。修改SQL无需重新编译调整缩进和条件注释也方便DBA也能直接拿过去审。2.2 MyBatis核心组件和生命周期很多人在面试时被问MyBatis核心组件回答得零散。其实就四个大件SqlSessionFactoryBuilder启动时读取配置文件创建工厂。绝大多数项目会在Spring容器里只初始化一次因为它本身重量级构建成本高。SqlSessionFactory全局单例负责创建SqlSession。它对应数据库的全局配置包括数据源、事务、二级缓存等。SqlSession每次数据库会话对应一个实例用完后必须关闭。它不是线程安全的所以不能设计成单例共享。Mapper绑定在SqlSession上的接口代理通过它操作数据库。实际上Mapper对象内部还是调用SqlSession完成操作。最简单的工作流程是应用启动时SqlSessionFactoryBuilder读取mybatis-config.xml或Spring Boot的application.yml中的配置外墙就是Configuration对象。之后每次请求需要操作数据库时从工厂拿一个SqlSession再从SqlSession拿Mapper接口的代理对象执行方法后关闭会话。也正因为这个生命周期Spring集成的时候会把SqlSessionTemplate做成线程安全的、并且管理事务的代理替代原生SqlSession。如果你在Spring Boot项目里手动new了一个SqlSessionFactory或者乱注入SqlSessionFactory很容易出现连接关闭异常。推荐直接使用MyBatis官方starter让容器管理生命周期。对于源码阅读的人来说建议从DefaultSqlSession#selectOne看进去。你会看到Executor、MappedStatement、StatementHandler、ParameterHandler、ResultSetHandler这套经典链。理解这条链等于理解了MyBatis源码百分之六十。3. Mapper接口与XML映射设计3.1 接口绑定XML的两种方式MyBatis里一个Mapper接口对应同名的XML文件是约定但不是必需。很多人用MyBatis Generator生成代码默认会在同包下生成相同的XML这是因为MyBatis的MapperScannerConfigurer扫描接口后会按接口的全限定名去类路径下找同名XML资源。实现绑定时有两种常见配置XML方式在mybatis-config.xml里通过mappers标签一个个引入。Spring Boot里一般用MapperScan扫描接口包并且设置mapper-locations指向XML目录比如classpath:mapper/*.xml。注解方式直接在Mapper接口方法上写Select、Insert等。适合SQL简单、不需要动态SQL的场景。一旦SQL复杂注解里的字符串拼接可读性就很差。我在实际项目里基本以XML为主注解只用来做少量简单查询。原因有三个XML可以用sql标签抽取公共列、公共查询条件复用性强。XML里的SQL可以进行格式化便于DBA审查。MyBatis的动态SQL标签在XML里配合if、where、foreach等比注解里搞拼接可读性好太多。如果你用了注解又想调整必须重新编译代码而XML调整后配置了热部署spring-boot-devtools里可以设置模板重新加载就能生效。所以保守的团队都会把SQL集中在XML。3.1.1 Mapper接口和XML的命名绑定规则注意一个容易出错的命名问题XML文件的namespace必须写成Mapper接口的全限定名而且XML中每个statement的id必须与接口方法名一致。如果namespace错了启动时扫描不到就会直接报错。举一个示例public interface UserMapper { User selectUserById(Long id); }对应的XMLmapper namespacecom.example.mapper.UserMapper select idselectUserById resultTypecom.example.entity.User select id, name, age from user where id #{id} /select /mapper这里实体类属性是id、name、age数据库列也恰好同名所以resultType能自动映射。如果字段名不匹配就得用resultMap或者在SQL里加别名。这是MyBatis默认行为它开启驼峰转换的配置后user_name能自动映射到userName但纯下划线到驼峰这个配置需要额外开启。3.2 #{} 与 ${} 参数占位符的区别这是MyBatis面试问题里出镜率最高、也最容易被误解的一个知识点。#{}是预编译参数占位符。MyBatis会把它解析成JDBC中的?使用PreparedStatement给参数赋值。例如where id #{id}在预编译时变成where id ?然后框架通过setString或setLong设置值。这样做的好处是能防止SQL注入因为参数值不会参与SQL语法解析。${}是做字符串替换也就是直接把传入的值拼接到SQL里。比如order by ${orderColumn}会变成order by name。它适合传入列名、排序关键字等无法用占位符表示的场景。但危险在于如果用户输入被直接拼到SQL里就可能被注入。比如${id}传入1 or 11SQL就变成了where id 1 or 11。所以能用#{}的绝对不要用${}。如果必须用${}要对输入做严格的校验或白名单映射。另外还有一个细节#{}里除了简单参数名还能指定jdbcType比如#{createTime,jdbcTypeTIMESTAMP}。当参数可能为null时不指定jdbcType有的数据库驱动可能不能识别null类型而报错。Oracle驱动对null的表示要求比MySQL严格。在老项目切换数据库遇到传null报错的时候第一反应就是检查#{}的jdbcType。3.3 动态SQLif、where、set、foreach的使用心得动态SQL是MyBatis非常受欢迎的特性。大部分实际项目里动态条件查询和批量操作几乎离不开它。if和where算是黄金搭档。很多人喜欢这样写select idsearchUsers resultTypeUser select * from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if testage ! null and age #{age} /if /where /selectwhere标签会自动处理首个子句前的and避免出现where and name的语法错误。不过我有个习惯不管标签能不能处理SQL里第一个动态条件也倾向于显式写上and因为在MySQL和Oracle中where加上以and开头的从句是合法的很多数据库优化器能正确处理。set用于更新语句自动去除最后的逗号。它的典型场景是动态updateupdate idupdateUser update user set if testname ! nullname #{name},/if if testage ! nullage #{age},/if /set where id #{id} /updateforeach用于in条件或批量插入。批量插入的核心参数是collection、item、separator。如果Mapper接口方法是直接传入一个ListLong那么collection固定为list如果是Param(ids) ListLong ids那collection写ids。很多人错在collection名字上启动时看具体报错就能明白。在我维护的商城项目里批量插入订单商品明细单条SQL插入上千行的场景常有。这里要注意数据库对SQL长度、参数个数尤其Oracle的IN列表有1000个限制都有约束。所以foreach批量插入要分批每批500到1000条数据比较合适。3.4 resultMap结果映射嵌套查询与嵌套结果对于单表查询用resultType就够了。但遇到多表关联想要返回VO对象resultMap就降到关键位置。最基础的是处理字段名映射。比如数据库列create_time实体类的属性是createTime可以通过resultMap的result columncreate_time propertycreateTime/显式映射。也可以在全局配置里打开map-underscore-to-camel-case自动实现这个转换。我还见过不少项目用select字段别名的方式来简化映射比如select create_time as createTime。这种做法快速但select列表和实体类之间的对应关系不直观而且如果SQL被复用别名很冗余。resultMap真正复杂的是嵌套映射比如订单实体里又包含用户对象、订单明细列表。有两种方式嵌套结果映射Nested Results一次SQL关联查询通过association和collection把结果拆到不同对象。它性能较好但SQL写起来复杂有时需要discriminator找分支。嵌套查询Nested Queries在association或collection的select属性指向另一个SQL每次查主表后再执行子查询。优点是SQL简单但这会产生N1问题查询大量主记录时性能极其堪忧不建议在列表页使用。刚才提到的N1问题在ORM里很常见。假设你查询10个订单每个订单又要额外查一次用户、查一次明细那实际SQL会执行 1 10 10 21次网络多次往返和数据库负载都会成倍增加。解决方案通常是写一个专门的JOIN SQL用嵌套结果映射来一次查出。后续章节我会写到一个实例。3.5 分页插件原理与手写分页的注意点MyBatis没有内置分页功能。早期很多人用RowBounds但实际上它是一次性查出所有数据后在内存里截取数据量大时就是性能炸弹。现在主流方案是分页插件比如PageHelper或者自己实现MyBatis插件。插件的原理是使用MyBatis的Interceptor机制拦截Executor的query方法。因为MyBatis执行query时可以拿到MappedStatement插件会在执行前通过BoundSql获取原始SQL然后改写为带limit的方言SQLMySQL或使用rownumOracle同时执行一个count查询得到总条数。PageHelper在ThreadLocal里保存分页参数执行时自动生效。我个人的建议是对于新项目可以尽量使用分页插件但要搞清楚它的使用方法避免出现分页参数串线程的问题。比如PageHelper必须在startPage后紧跟第一个MyBatis查询中间不要混杂其他查询。否则后续无关查询都会被分页很多人踩过这个坑。如果不想引入插件也可以写一个通用方法。这里分享一个简单的MySQL分页写法select idfindUsersByPage resultTypeUser select * from user where 1 1 if testkeyword ! null and name like concat(%, #{keyword}, %) /if order by id desc limit #{offset}, #{pageSize} /select但要注意当排序字段和索引不匹配时深分页比如偏移到第100万条会很慢。常见优化办法是延迟关联先查ID再关联回表。这条经验在分页大数据集里非常实用。4. 缓存机制深度解析4.1 一级缓存SqlSession级别的默认缓存MyBatis默认启用一级缓存也就是SqlSession级别的缓存。同一个SqlSession里相同条件的查询结果会被缓存第二次查询直接命中缓存不会再次访问数据库。但必须注意一级缓存的作用域是SqlSession而Spring集成MyBatis后每个请求通常默认开启一个新的SqlSession所以一级缓存很难跨多次操作复用。除非在一个事务里SqlSession被固定为同一个此时事务范围内的相同查询会走一级缓存。这是好事还是坏事要看场景。对单个方法内多次查询相同数据一级缓存能省一次数据库访问。但如果你在一个长会话里先查询用户再通过其他途径更新了用户又去查询一级缓存里还是旧数据。这种脏数据问题在未开启事务的普通场景下很难出现因为每次会话用完就关闭但在手动管理SqlSession的代码里特别容易踩。一级缓存的失效时机是执行任何增删改操作以及调用SqlSession.clearCache()。因为增删改可能导致缓存数据不完整MyBatis默认会清空一级缓存。很多人在排查缓存问题时第一个动作就是往对应Mapper里加个flushCachetrue或清缓存这种粗暴操作确实能解决但会失去原本的缓存好处。4.2 二级缓存跨SqlSession的应用级缓存二级缓存是Mapper级别的缓存作用域是SqlSessionFactory。也就是说多个SqlSession操作同一个Mapper时可以共享缓存。二级缓存默认不开启需要手动配置。开启步骤很简单在mybatis-config.xml设置全局参数cacheEnabledtrue默认就是true但必须开着才能用。在Mapper的XML里加入cache/标签表示本Mapper使用二级缓存。cache/标签可以配置几个属性例如eviction回收策略、flushInterval刷新间隔、size最大缓存对象数量、readOnly是否只读。最常用的策略是LRU即最近最少使用。配置示例cache evictionLRU flushInterval60000 size512 readOnlytrue/然后该Mapper下所有select语句默认都会使用二级缓存而所有增删改默认都会刷新缓存。但有个重要限制二级缓存缓存的必须是对象经过序列化后的值默认对象需要实现Serializable或者配置了自定义序列化。另外如果一个Mapper的二级缓存刷新了但关联的另一个Mapper查询SYS数据没有刷新就会出现数据不一致。跨表join的查询尤其要注意。4.3 二级缓存实现细节与源码视角从源码去看二级缓存其实很直白。每次查询时MyBatis的Executor会被包装成CachingExecutor如果开启缓存。CachingExecutor#query先根据MappedStatement.getId()和查询参数计算缓存keyCacheKey到MappedStatement对应的PerpetualCache中查找。如果没找到才委派给实际的BaseExecutor执行数据库查询并把结果放到缓存。这里的缓存key设计很有讲究。它是由statementId、分页参数、SQL、SQL参数、环境等信息拼接出来的。所以不同参数、不同SQL不会互相覆盖。二级缓存可能出现的问题有两个第一个是脏数据问题刚才说过。第二个是并发场景下缓存不一致。MyBatis默认的PerpetualCache本质是HashMap没有分布式缓存的能力。如果你做了多实例部署每个应用实例各自维护一份二级缓存一实例更新数据库后另一个实例的缓存没有失效就会读到脏数据。所以很多生产环境的分布式架构反而会关闭二级缓存把热点数据交给Redis统一管理。如果你想在多个服务之间共享缓存或者做更精细的过期策略可以自定义Cache实现类配置到cache typecom.example.MyRedisCache/。实现时需要实现MyBatis的Cache接口包括getObject、putObject、removeObject、clear、getSize等方法。但这属于比较高级的用法一般项目用不到涉及序列化、分布式锁、缓存穿透等问题除非团队有专门基础架构支持否则不建议贸然引入。4.4 缓存相关面试题和避坑经验关于MyBatis缓存面试官一般会问“一级缓存什么时候失效”“二级缓存为什么默认不开启”“你怎么解决缓存脏数据”。回答的时候要落到“SqlSession生命周期”和“多环境并发”两个核心。我实际项目里比较常用的是把二级缓存彻底关掉用Redis做热点数据Cache。这样既避免MyBatis缓存与业务数据不同步的问题也能支撑高并发。但有个场景特例纯词典表比如国家、省份、性别字典几乎不更新用MyBatis二级缓存能提升不少。这时需要注意更新字典数据时要么手动刷新缓存要么设置合理的flushInterval。另外使用二级缓存时查询结果对象必须是可序列化的。如果对象里有LocalDateTime、枚举等不同版本JDK/序列化框架的兼容性也要测一遍。曾经遇到一个项目启动正常一开二级缓存就报NotSerializableException排查了半个小时发现查询对象里有个自定义类型没实现序列化。这类问题配置了缓存之后一定要做一次回归测试。5. Spring Boot集成与SQL日志配置5.1 官方starter整合步骤现在开发Spring Boot应用集成MyBatis已经非常简单。引入依赖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然后在application.yml里配置数据源和MyBatis相关属性。一个典型配置spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl注意mapper-locations这个配置非常关键。如果遗漏MyBatis扫描不到XML文件启动后调用Mapper方法会报Invalid bound statement (not found)。这也是Spring Boot集成MyBatis最常见的错误之一。检查的时候要看编译后的target/classes/mapper目录下有没有XML如果资源没有被复制过去还要检查pom.xml的build资源声明。5.2 打印SQL的几种姿势开发和排查问题时打印SQL必不可少。热词里有“mybatis配置打印”这里说透。最常用的方式是设置log-impl: org.apache.ibatis.logging.stdout.StdOutImpl它会把日志直接输出到控制台非常直观但是生产环境别用类名和行为都太重。更规范的方式是配合配合SLF4J的日志级别。MyBatis的log-impl可以设置为Slf4jImpl然后通过配置日志输出级别只对指定的Mapper打印SQLlogging: level: com.example.mapper: debugMyBatis会以debug级别打印执行过程包括Preparing、Parameters、Total。这样在生产环境也可以针对性调试单个Mapper的SQL而不用全量打印。还有一个高级功能通过增加mybatis.configuration.interceptors或者自定义Interceptor拦截器打印SQL执行时间和参数。我用的比较多的是MyBatis Plus或优秀的分页插件自带的SQL日志分析但如果项目用的是原生MyBatis写一个简单的拦截器也不复杂。得益于org.apache.ibatis.plugin.Interceptor的签名Intercepts({Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})你可以在query方法前后获取时间差再解析BoundSql.getSql()拿到原始SQL。注意解析SQL时要处理参数替换和格式化否则输出连?看着难受。我写了一个小工具把ParameterHandler里获取的参数值按顺序替换到SQL里对调试非常有用。5.3 Spring Boot下多数据源与事务注意事项Spring Boot集成MyBatis支持多数据源最简单的是在配置里定义多个DataSource和SqlSessionFactory然后通过MapperScan指定不同Mapper包对应不同工厂。但多数据源下最容易出问题的是事务。默认的Transactional会绑定到主数据源PrimaryDataSource如果操作第二个数据源事务不生效。一种更稳妥的多数据源方案是使用AbstractRoutingDataSource动态路由或者引入成熟的框架如baomidou的dynamic-datasource-spring-boot-starter。用DS(slave)切换数据源事务也能正确管理。另一个常见问题是Mapper接口的扫描范围。如果一个Mapper接口出现在了多个MapperScan中或者XML的namespace冲突启动会异常。多数据源时尽量按业务模块划分不要缠在一起。6. MyBatis在开源多商户跨境商城项目中的实战要点6.1 为何这类商城项目偏爱MyBatis热词里提到“spring boot mybatis 的 java 开源多商户跨境商城源码下载”。虽然我不能替你下载源码但可以聊一聊这类项目为什么普遍选择MyBatis以及你在读源码时应该重点关注什么。多商户商城系统的数据模型相当复杂有平台端、商户端、用户端涉及商品、订单、支付、物流、优惠券、会员、财务对账等模块。数据访问层需要大量复杂的动态查询例如按商户和商品分类过滤、订单状态统计、分页报表。MyBatis的动态SQL能很好地应对这种变化每个模块的SQL可以按业务聚合到Mapper里后期调整一个统计口径只需要改相应的XML即可。同时商城项目上线后运营经常会提按条件导数据组合查询的需求用MyBatis的where加if组合做条件筛选非常顺手。另外商城中经常要做金额核对、订单幂等、库存扣减这类事务敏感操作。MyBatis不帮开发者做太多的懒加载和隐式提交反而让事务边界更好控制。你可以精确控制每条SQL的执行时机在Transactional方法内保证同一连接不容易出现ORM中flush过早的问题。6.2 多商户跨境项目的特定数据访问难点跨境商城与普通商城的区别在于它涉及更复杂的国际结算、海关申报、多币种、多语言、多仓库库存等字段。以订单为例可能有订单表、订单商品表、订单支付流水表、物流报关表。查询一个订单详情时通常需要返回一大套嵌套对象。如果用纯resultType就得在Java里手动组装代码繁琐用MyBatis的resultMap嵌套映射SQL可以用JOIN一次查完。举个例子订单详情VO包含基本信息、买家信息、商品列表、支付列表、物流信息。我会定义一个OrderDetailVO属性中包含ListOrderItemVO items、ListPaymentVO payments。在XML里配置resultMap idOrderDetailMap typecom.example.vo.OrderDetailVO id propertyorderId columnorder_id/ result propertyorderNo columnorder_no/ collection propertyitems ofTypecom.example.vo.OrderItemVO id propertyitemId columnitem_id/ result propertyskuId columnsku_id/ result propertyamount columnamount/ /collection collection propertypayments ofTypecom.example.vo.PaymentVO id propertypaymentId columnpayment_id/ result propertypaymentMethod columnpayment_method/ /collection /resultMapSQL里要保证每行数据的item_id和payment_id有值否则MyBatis判断嵌套对象时可能会有意想不到的结果。一般来说这种JOIN查询一次SQL能解决但数据量大时也要注意SQL性能和结果集内存占用。如果订单详情页每天大量请求建议把结果做Redis缓存。实际商城项目里还有一类高频操作按照订单状态和下单时间进行分页查询。这种SQL必须建索引分页插件使用时要留意PageHelper.startPage和count查询的消耗。当数据量达到百万级count语句也可能拖慢页面因此有的团队会单独维护统计表或者限定默认查询时间范围。6.3 从电商项目中学到的MyBatis优化思路在多商户商城项目里我总结出几条数据访问层的优化经验放在这里一并分享。第一条能用批量操作就不要循环单条。例如保存订单商品明细如果循环调用Mapper.insert会产生大量SQL执行和事务开销直接改为foreach批量insert。但注意批量插入时事务会把整个批次包裹在一个事务里如果一条数据异常会全部回滚要根据业务决定批次大小。第二条查询列表时只查需要的字段。很多人习惯了select *在商城列表页如果商品有几十个字段全部查出来不仅占用网络带宽也增大内存和CPU开销。建议用resultType指定DTO只select需要的列。第三条深分页优化。列表页超过深分页比如offset 500000时MySQL需要扫描大量行再丢弃。一种解决方式是先查子查询只取ID再外层关联select * from user u join ( select id from user order by id limit #{offset}, #{pageSize} ) tmp on u.id tmp.id这种方式在特定条件下比直接limit性能好很多。当然不同数据库支持程度不同像Oracle还需要借助rownum或ROW_NUMBER()处理。第四条合理运用二级缓存或Redis。商城的热点数据如首页商品列表、分类导航变化频率低可以直接缓存到Redis订单类数据变化频繁不建议放MyBatis二级缓存因为订单写入多缓存不断失效命中率低且容易脏读。7. 源码阅读与面试题实战攻略7.1 如何高效阅读MyBatis源码想深入理解MyBatis看源码绝对是最直接的方式。但源码有几千个类从头啃会很崩溃。我的建议是沿着一次完整查询的调用链去读。首先从SqlSessionFactoryBuilder.build()开始看配置文件如何被解析成Configuration对象。然后调用SqlSessionFactory.openSession()创建DefaultSqlSession注意它内部持有Executor。再看getMapper()如何通过MapperProxyFactory生成代理MapperProxy.invoke()会把接口方法转换为MapperMethod最终调用SqlSession.selectOne/selectList/insert/update/delete。以selectList为例DefaultSqlSession.selectList从Configuration获取MappedStatement。若二级缓存开启Executor会是CachingExecutor先查缓存。未命中缓存则调用BaseExecutor.query从LocalCache一级缓存查缓存。仍未命中则进入SimpleExecutor.doQuery创建StatementHandler。StatementHandler.prepare创建PreparedStatement。ParameterHandler.setParameters设置参数。ResultSetHandler.handleResultSets将ResultSet映射成结果列表。读源码的时候建议边读边画调用链条用文字列下来看完每个类再看方法的注释。重点理解BoundSql和MappedStatement两个数据结构它们分别代表了“最终要执行的SQL”和“一条Mapper方法对应的信息”。如果你之前没有读过源码可以从MyBatis 3.5.x版本入手因为它的代码结构比老版本清晰很多注释也相对完整。7.2 MyBatis高频面试题梳理结合热词和实际面试常见的问题整理成表格方便大家快速过一遍面试题关键回答要点MyBatis和Hibernate的区别MyBatis半自动SQL由开发人员控制灵活度高Hibernate全自动开发效率高但复杂查询优化难。#{} 和 ${} 的区别#{}预编译占位符防注入${}字符串拼接适合列名等字段有注入风险。一级缓存和二级缓存一级缓存SqlSession级别默认开启二级缓存Mapper级别默认关闭需要配置 。Mapper接口如何绑定XMLnamespace为接口全限定名statement的id为方法名。动态SQL有哪些标签if、where、set、foreach、choose、when、otherwise、trim、sql片段。分页插件原理拦截Executor改写SQL为方言分页SQL并执行count查询。如何避免SQL注入使用#{}、对${}参数做白名单校验、不使用拼接字符串SQL。MyBatis的一级缓存生产环境是否安全注意SqlSession生命周期Spring容器下事务内有效不要手动共享SqlSession。resultType和resultMap区别resultType自动映射resultMap支持嵌套映射、字段映射自定义。MyBatis是如何做到接口方法调用即SQL执行的动态代理MapperProxy结合MapperMethod解析方法名和SQL。更多更深的面试题比如“MyBatis的拦截器顺序”“PageHelper分页后为什么需要紧跟查询”都可以在实践中学到。7.3 快速定位线上慢SQL与优化手段线上环境最常遇到的问题之一是慢SQL。MyBatis项目定位慢SQL相对容易把sql日志打开后能直接看到每次执行的SQL。我这里有个排查流程供参考第一步确认SQL是哪个Mapper方法触发的。可以从日志中的Preparing看到SQL文本和对应的Mapper id。第二步把SQL扔到数据库执行EXPLAIN看有没有走索引、扫描行数。重点关注type列是否出现ALL或indexrows是否巨大。第三步分析是否需要调整索引、改写SQL、或改成批处理。如果是动态SQL很常见的坑是条件变化导致SQL没有命中联合索引。比如where status ? and create_time ?单独建了status索引和create_time索引但联合查询时优化器可能只选其中一个效率不高。这种情况下需要建联合索引(status, create_time)。还有一种场景是like %xx%导致索引失效可用全文索引或者搜索组件代替如果数据量不大直接接受全表扫描也行。另外MyBatis开启useGeneratedKeys获取数据库自增主键是日常必要操作。配置如下insert idinsertUser useGeneratedKeystrue keyPropertyid insert into user(name, age) values(#{name}, #{age}) /insert这样插入后实体对象的id属性会自动回填。在商城项目里先插入主表获取到主键再插入子表关联数据是非常典型的用法。需要注意keyProperty对应的是实体属性名而不是数据库列名。这是新手最常见的错误。7.4 向Spring Boot三方扩展工具借鉴经验现在很多项目直接用MyBatis-Plus它把单表CRUD、条件构造器、分页都封装好了。但我的看法是先学原生MyBatis再看MyBatis-Plus否则很多问题都是黑盒。比如MyBatis-Plus的selectPage用的分页插件与原生PageHelper类似你需要理解它的实现原理才能防止分页失效和内存泄漏。如果项目里有复杂单表CRUD和频繁的代码生成可以考虑引入MyBatis Generator或MyBatis-Plus Generator生成基础实体、Mapper接口和XML。但要控制好生成的代码质量和风格避免整个项目充满冗余的方法。我见过有的新项目把Generator生成的十几个方法全部保留实际只用其中三四个把XML撑得特别大。建议按需裁剪甚至改为自定义通用Mapper接口。另一个值得学习的拓展是MyBatis 3.5的ResultHandler机制。在执行大量数据处理时可以通过自定义ResultHandler逐行处理结果集而不是一次性把所有数据放进List。比如导出千万级数据时这种流式读取非常有用。结合StatementHandler可以设置ResultSet.TYPE_FORWARD_ONLY和fetchSize避免OOM。8. 常见问题与排查技巧实录8.1 “Invalid bound statement (not found)”的排查这个问题在Spring Boot项目里出现频率极高。报错信息说找不到SQL绑定但你的Mapper接口和XML明明都有。核心排查思路是看几方面启动类上有没有MapperScan(com.example.mapper)如果有多个包可以写数组形式。XML文件是否在类路径下。Spring Boot默认只把src/main/resources下的文件打进包如果你的XML放到src/main/java或被自定义配置排除就没被扫描到。检查target/classes/mapper目录。XML的namespace和statement id是否拼错。application.yml的mapper-locations是否覆盖了XML路径。我遇到过一次诡异的情况本地运行正常部署到服务器后报not found。最后发现是打包插件把XML过滤掉了pom里的resource配置互相覆盖。把resources配置重新指定即可。8.2 参数映射错误There is no getter for property named xxx当你在XML里写#{username}但传入的参数是一个MapMap里没有username这个key就会报这个错。或者传入一个实体对象但对象的字段名拼写错误也会发生。排查方法很简单先看接口方法签名确认是单个参数还是多个参数。单个JavaBean对象的属性名拼写要检查getter是否存在。还要注意一个细节如果Mapper方法只有一个基本类型参数XML里写#{value}或#{任意名字}都能取到值因为MyBatis会把单参数包装成_parameter支持多种引用。但多参数时就必须使用Param注解或者在接口参数上写对应名字否则只能用arg0、param1这种固定命名规则。我强烈建议无论参数多少都用Param显式命名可读性差不了多少还不会踩坑。8.3 事务失效与连接占用问题MyBatis和Spring事务配合时容易出的问题有两个。第一个是Transactional方法没生效。典型场景是自己new了一个Mapper对象而不是通过Spring代理注入。或者配置了多个SqlSessionFactory事务管理器没有正确指定哪个数据源。排查时先看启动日志里是否创建了多个DataSourceTransactionManager。第二个是连接被占用。如果一个方法里循环查询很多次并且每次查询都通过SqlSessionTemplate开启但忘记关闭资源就会泄漏。Spring管理的SqlSessionTemplate虽然会自动关闭但你在方法里如果直接获取了SqlSession并调用selectList没有在finally里close会导致连接无法归还。我印象很深的一个线上问题压测时发现连接池被占满但业务量并不大。后来把堆栈dump下来发现是一些老代码中直接使用了SqlSession忘记关闭。这再次说明MyBatis的生命周期管理不能大意。8.4 缓存脏数据的实战根因假设你的Mapper A查询了一个列表另一个Mapper B更新了和这个列表关联的数据A的二级缓存并不会自动失效。你把二级缓存当全局缓存用就容易读到旧数据。我之前做过一个产品详情模块产品基本信息存在MapperProductMapper产品库存存在StockMapper。购买后库存发生变化但ProductMapper的二级缓存里还缓存了包含库存的详情VO导致用户端看到的库存一直没有更新。最后解决方案就是要么把详情查询放到一个独立的Mapper其缓存刷新时机与库存更新绑定要么完全移除二级缓存把详情数据交给Redis用库存变更消息主动删除缓存。踩过这个坑后我通常建议项目保持简单不要过度信赖MyBatis自带的二级缓存。9. 项目落地后的个人经验与后续扩展如果让我总结MyBatis项目的落地经验最核心的一句话是把SQL当作一等公民来管理。MySQL和Oracle的语法、分页、批量插入方式各不相同所以项目里我会把SQL集中在XML中并通过命名规范快速定位。在团队协作时我会要求所有Mapper方法、XML id都按照业务模块命名不允许出现无说明的自定义SQL生成。对复杂SQL必须加注释标明关联表的主键关系、索引依赖和业务含义。这样即使半年后换人维护也能快速接手。关于后续扩展我认为有两个方向值得投入。第一个是配置mybatis-plus或通用Mapper相关组件提升单表CRUD效率。第二个是实现一套方法级慢SQL告警比如通过自定义拦截器把超过指定时间的SQL记录到日志或者发送到监控平台。这个拦截器写起来很简单在Executor.query前后计算耗时超过阈值就输出完整SQL。另一个想分享的小技巧是使用MyBatis的sql标签抽取公共列。比如很多查询都需要select id, name, age, create_time from user你可以定义一段可复用的公共SQLsql iduserColumns id, name, age, create_time /sql然后在你各个select中引用include refiduserColumns/。这样当表的字段变更时只需要修改一处公共sql片段。这个方式尤其适合上游换了表结构后需要统一改字段名的场景。我在老项目里几百条SQL需要统一加一个字段就是靠这种片段实现的没有出任何差错。最后再说说面试里一个值得深挖的知识点“如果你来处理一个超大表的全表扫描你会怎么用MyBatis”这个问题没有标准答案但你可以从懒加载、流式读取、分页、SQL优化多个角度展开。实际上你平时踩过的每一条MyBatis的坑面试时都能变成加分项。我自己在面试候选人的时候最想听到的并不是背出来的原理而是他真实处理过什么规模的数据SQL在哪里出了问题又是怎么定位和解决的。这篇博文就是我从实际项目中攒下的MyBatis使用心得从缓存到分页到源码视角都讲了一遍。如果你正在用MyBatis做业务开发建议把今天提到的几个关键点对照自己项目检查一遍resultMap的嵌套是否合理、分页参数有没有串、二级缓存该不该开、慢SQL有没有被打印出来。数据访问层是系统的地基地基里每一条SQL的质量最终都会体现在线上请求的响应时间上。把MyBatis用透比多引进一个框架重要得多。
返回列表