MyBatis从配置到SQL操作全链路解析:动态SQL、结果映射与缓存实践
发布时间:2026/9/9 3:05:57来源:尧图网络
如果你写Java超过半年大概率已经和MyBatis打过交道。但说实话真正把MyBatis配置文件里每个标签、SQL映射里每条动态拼接规则都吃透的人并不多。很多人一上来就依赖MyBatis-Plus的BaseMapper写简单CRUD确实快可一旦遇到复杂查询、多表联查、批量更新、结果集映射不对、缓存读脏数据这些问题连从哪儿排查都不知道。这篇内容不打算只给你贴一堆配置片段而是把“数据库配置”和“SQL操作”这条主线完整过一遍从全局配置文件mybatis-config.xml的核心标签到Spring Boot的整合方式再到动态SQL、结果映射、缓存机制、自动建表、日志打印和源码执行的完整链路全部串联起来。既适合刚入门的朋友照着配置和复现也适合工作两三年的开发者用来查漏补缺顺便把面试题里那些高频问题彻底弄明白。1. 先理顺MyBatis到底解决了什么问题1.1 从JDBC的痛苦说起在MyBatis之前Java操作数据库主要就是JDBC。JDBC不是不能用而是用起来太啰嗦。每次查询都要手动注册驱动、获取Connection、创建PreparedStatement、设置参数、执行查询、遍历ResultSet、然后还要逐个字段getString、getInt转成对象最后还要关闭连接、关闭语句、关闭结果集。一套固定流程写下来跟复制粘贴没什么区别而且这些样板代码占据了开发时间的大头。更难受的是参数和结果集的映射查出来一列列数据你得自己按位置取值数据库字段改名了、Java属性类型变了代码里到处要跟着改。SQL语句一旦写错编译期发现不了运行时才报错排错全靠日志一点点看。这些问题累积起来就催生出了“ORM框架”这个解决思路。Hibernate走的是全自动路线把Java对象和数据表完全映射让你基本不写SQL操作对象就等于操作数据库。但全自动带来的问题是SQL不可控复杂查询经过Hibernate转换后性能和可读性都难保证。MyBatis走了另一条路SQL由开发者自己写框架只负责帮你完成参数绑定、结果映射、连接管理等周边工作。这种方式在复杂SQL和需要调优的场景下优势很大也是国内很多团队最终选择MyBatis的原因。1.2 MyBatis的核心定位SQL与Java代码解耦MyBatis的核心设计思路就是“半自动”SQL是你的框架帮你把琐碎的事情做了。你的Java代码只需要定义Mapper接口、声明方法然后在XML文件里写SQLMyBatis会按照规则把方法和SQL关联起来。具体来说它做到了这么几件事参数映射自动化你方法里传一个对象或者几个参数MyBatis通过#{}或者${}语法把它绑定到SQL里。结果集映射自动化查询返回的ResultSet通过resultType或者resultMap配置自动封装成Java对象、List、Map。SQL与业务代码解耦SQL整体放在XML里Java代码不直接接触SQL字符串修改SQL不需要重新编译Java代码重新加载映射文件就能生效。连接管理统一数据库连接的获取、提交、回滚、关闭都由框架统一处理你不用再写一堆try-catch-finally。这种设计天然适合SQL优化和团队协作。DBA或者后端老手可以直接在XML里调整SQL不用担心动到Java代码新人也只需要照着映射规则写方法不用关心底层连接细节。1.3 和MyBatis-Plus怎么选每次聊MyBatis都会遇到“为什么不用MyBatis-Plus”这个问题。这里给个客观结论MyBatis-Plus是MyBatis的增强工具它在不改变MyBatis框架运行逻辑的前提下内置了通用Mapper、通用Service、条件构造器Wrapper、分页插件、代码生成器等能力最典型的就是继承BaseMapper后直接有selectById、insert、updateById这类方法。选择上我的建议是简单管理系统、CRUD占比高的项目用MyBatis-Plus提升开发效率非常明显但对SQL有严格管控要求、团队想保持最纯粹MyBatis技术栈、或者对第三方增强依赖有顾虑的时候原生MyBatis更合适。MyBatis-Plus虽然好用但它的条件构造器一旦嵌套复杂生成的SQL可能不如手写直观排查问题的时候你又得回到“开启SQL日志打印”这条路。所以不管最后项目选哪个原生MyBatis的配置和SQL操作体系都值得彻底搞懂。懂了底层再用增强框架遇到问题定位会快得多。2. 数据库配置体系从mybatis-config.xml到Spring Boot2.1 mybatis-config.xml全局配置主标签如果你用过早期的MyBatis一定见过这个文件。它是MyBatis的全局配置文件定义了框架运行时的各种行为。下面这张表把核心标签的作用整理清楚了。标签作用典型配置说明settings设置全局运行行为驼峰映射、缓存开关、日志实现、超时时间等typeAliases缩短类全限定名的别名给实体类起别名XML里就不用写全路径typeHandlers自定义类型转换器处理特殊数据类型与Java类型的转换environments配置环境如开发、测试、生产每个环境里配置事务管理器、数据源mappers注册Mapper映射文件告诉MyBatis去哪里加载SQL映射XML重点说settings里那些高频项。最实用的就是mapUnderscoreToCamelCase数据库字段一般是user_nameJava属性通常是userName把这个配置设为trueMyBatis在映射结果集时会自动做下划线转驼峰少写一堆resultMap。还有cacheEnabled控制二级缓存开关localCacheScope控制一级缓存范围logImpl指定日志实现defaultStatementTimeout设置SQL执行的默认超时时间。environments现在在Spring Boot整合中确实不常直接使用了因为数据源和事务管理都交给了Spring容器。但理解它的存在对看老项目和MyBatis源码有帮助。一个environment包含一个事务管理器transactionManager和一个数据源dataSourceJDBC事务管理器是你自己控制commit、rollback、closeMANAGED事务管理器是交给外部容器去管。整合Spring后MyBatis会把事务控制权委托给Spring的DataSourceTransactionManager这也是为什么你在Spring项目里不用手动控制事务的原因之一。给一个最小可用的全局配置示例感受一下结构?xml version1.0 encodingUTF-8 ? !DOCTYPE configuration PUBLIC -//mybatis.org//DTD Config 3.0//EN https://mybatis.org/dtd/mybatis-3-config.dtd configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings typeAliases package namecom.example.entity/ /typeAliases environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/demo/ property nameusername valueroot/ property namepassword valueroot/ /dataSource /environment /environments mappers package namecom.example.mapper/ /mappers /configuration这段配置在纯MyBatis项目中可以直接用。注意typeAliases里用package指定包后XML中写resultTypeUser就能映射到com.example.entity.User不用写全限定名。mappers里同样可以直接扫描包路径不用逐个注册XML文件。2.2 Spring Boot环境下的配置方式现在的项目几乎都是Spring Boot整合MyBatis配置文件从XML搬到了application.yml里但核心配置项的语义是一样的。依赖方面只要引入mybatis-spring-boot-starterMyBatis会自动完成核心组件的装配。你需要在application.yml里告诉框架Mapper接口在哪里、XML映射文件在哪里、日志要不要打印、驼峰映射开不开。一个典型的配置如下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 global-config: db-config: id-type: auto需要提醒几个容易踩的坑mapper-locations路径必须和XML实际存放位置一致。常见的悲剧是把XML放在src/main/java下但没配置构建插件结果打包后XML没被复制到classpath启动就报Invalid bound statement (not found)。configuration.map-underscore-to-camel-case是Spring Boot风格的写法不是mapUnderscoreToCamelCase当然在mybatis.configuration.*下面用驼峰属性名也是Spring Boot的绑定规则两种写法功能一样。不用写SqlSessionFactory和SqlSessionTemplate的Beanstarter已经帮你自动装配好了除非你有特殊需求才去手动声明。还有一点容易被忽略spring.datasource.url里的characterEncodingutf8和serverTimezoneAsia/Shanghai最好一开始就加上。不加字符集参数插入中文可能出现乱码不加时区参数连接MySQL 8以上的版本可能报时区错误。2.3 表不存在自动建表三种落地路径“Spring Boot MyBatis表不存在时自动建表”这个需求在本地开发环境特别常见尤其是多个同事共用一套代码、数据库又是新建的时候。我总结了三类靠谱做法。第一类用Spring Boot自带的SQL初始化Spring Boot支持在classpath下放schema.sql和data.sql分别用来建表和初始化数据。需要在配置里打开初始化开关spring: sql: init: mode: always schema-locations: classpath:sql/schema.sql >Component public class TableInitRunner implements ApplicationRunner { Resource private DataSource dataSource; Override public void run(ApplicationArguments args) throws Exception { try (Connection conn dataSource.getConnection(); Statement stmt conn.createStatement()) { ResultSet rs stmt.executeQuery( SELECT COUNT(*) FROM information_schema.tables WHERE table_schema DATABASE() AND table_name user); rs.next(); if (rs.getInt(1) 0) { stmt.executeUpdate( CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_name VARCHAR(64), age INT)); } } } }这种方式最灵活团队里如果已经有统一的初始化组件很容易融合进去。但要注意并发问题如果多个应用实例同时启动可能都判断表不存在然后都去建表导致其中一个失败。生产环境还是交给Flyway这类工具更稳妥。2.4 配置打印SQL日志排查问题第一步不管你是写动态SQL还是排查慢查询第一步永远是把实际执行的SQL和参数打印出来。MyBatis打印SQL有两种常见方式。在application.yml里配置log-impl是最直接的方法日志会输出到控制台mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这种方式输出格式简单直接能看到SQL语句、参数Parameters: zhangsan(String)、返回行数Total: 1。缺点是生产环境如果也开着日志量会很大。更推荐的方式是用日志框架的级别控制只对Mapper接口所在包开启DEBUG。比如使用logback时logging: level: com.example.mapper: debug这两种方式打印出的内容略有区别但都能看到SQL和参数。实际排查中我习惯先开StdOutImpl快速确认SQL拼接结果确认无误后切回logback的debug避免刷屏。另外IDE里有个很有用的工具叫MyBatis Log Free它能把MyBatis日志里那些带?占位符的预编译SQL还原成真实的SQL语句点一下就能复制到数据库客户端去执行验证排查动态SQL拼接问题时效率极高。3. SQL操作核心细节从参数绑定到结果映射3.1 #{}和${}的区别这是MyBatis面试必问题也是实际开发中最容易写出隐患SQL的地方。两者的区别归结为一句话#{}是预编译占位符${}是字符串替换。用#{}时MyBatis会把SQL编译成?占位符然后通过PreparedStatement设置参数。这样参数值永远不会直接拼接到SQL里天然防止SQL注入也是官方推荐的方式。比如select idselectByName resultTypeUser SELECT * FROM user WHERE user_name #{name} /select实际生成的SQL是SELECT * FROM user WHERE user_name ?然后参数值通过setString传入。用${}时MyBatis直接把值拼接到SQL字符串中不会做任何转义或参数化处理select idselectByColumn resultTypeUser SELECT * FROM user ORDER BY ${column} DESC /select这里如果column是用户输入的值那SQL注入风险非常高。所以${}只应该用在表名、排序字段、动态列名这类无法作为预编译参数的位置并且必须确保传入的值是受控的最好用枚举或白名单校验。面试时如果被问细节可以补充一句#{}在JDBC层会走PreparedStatement的setXXX方法参数值不会参与SQL语法解析${}则在SQL解析前就已经替换进去和直接写SQL没有区别。这也是为什么${}能做动态表名而#{}不能。3.2 动态SQL标签实操动态SQL是MyBatis最核心的招牌能力用得对不对直接决定项目代码是清晰还是混乱。写在XML里的SQL可以像写if-else一样拼装但比Java代码里手动拼接字符串优雅得多。这里把常用标签逐一拆开讲每个都结合实际场景。if标签最基础的判断标签。常见场景是按条件查询用户可能传name也可能传age但不固定select idselectByCondition resultTypeUser SELECT * FROM user WHERE 1 1 if testname ! null and name ! AND user_name #{name} /if if testage ! null AND age #{age} /if /select很多教程会写WHERE 1 1目的就是让后面所有if都能直接以AND开头避免第一个条件判断为false时SQL多出一个WHERE关键字导致语法错误。where标签WHERE 1 1虽然能跑但不少DBA看着不舒服。用where标签可以自动处理开头的AND或ORselect idselectByCondition resultTypeUser SELECT * FROM user where if testname ! null and name ! AND user_name #{name} /if if testage ! null AND age #{age} /if /where /selectwhere标签会在子元素有条件成立时输出WHERE同时自动去掉前面多余的AND或OR。它本质上是trim prefixWHERE prefixOverridesAND |OR 的语法糖。set标签更新操作里处理动态字段最好用set它可以自动去掉更新列列表末尾的逗号update idupdateUser UPDATE user set if testname ! null user_name #{name}, /if if testage ! null age #{age}, /if /set WHERE id #{id} /update如果两个if条件都不满足最终SQL会变成UPDATE user SET WHERE id ?这是有语法错误的。所以set里至少得保证有一个字段被更新通常可以先用id作为保底或者调用前在Service层判断。foreach标签处理IN查询和批量插入时foreach几乎是唯一顺手的选择。select idselectByIds resultTypeUser SELECT * FROM user WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach /select批量插入是另一个经典场景insert idbatchInsert INSERT INTO user(user_name, age) VALUES foreach collectionlist itemuser separator, (#{user.userName}, #{user.age}) /foreach /insert使用foreach时collection属性是关键如果Mapper接口方法参数是List默认可以写list如果参数加了Param(ids)注解就必须写注解里的名字。我见过太多人栽在这一步明明参数名是ids却还在用list结果运行时报There is no getter for property named list。choose、when、otherwise标签相当于Java里的switch-case多个条件只取一个。比如查询时优先按id查没有id再按name查select idselectByChoose resultTypeUser SELECT * FROM user where choose when testid ! null id #{id} /when when testname ! null and name ! user_name #{name} /when otherwise status 1 /otherwise /choose /where /selectbind标签主要用于模糊查询中变量预处理。CONCAT(%, #{name}, %)在MySQL下没问题但换到Oracle就不一定了。bind可以在XML里先定义一个变量避免SQL写死拼接语法select idselectLike resultTypeUser bind namelikeName value% name %/ SELECT * FROM user WHERE user_name LIKE #{likeName} /selectsql和include标签当多个查询都要查同一组字段时抽出来复用sql iduserColumns id, user_name, age, status /sql select idselectById resultTypeUser SELECT include refiduserColumns/ FROM user WHERE id #{id} /select修改了列名或新增字段只需要改这一个sql块省心很多。3.3 resultMap结果映射动态SQL解决的是“查”的拼装问题而结果映射解决的是“取”的转换问题。正常情况下只要数据库字段和Java属性严格对应用resultType就够了。但实际项目中总会遇到字段对不上的情况这时就要用resultMap显式告诉MyBatis每个列映射到哪个属性。resultMap iduserResultMap typeUser id propertyid columnid/ result propertyuserName columnuser_name/ result propertyage columnage/ /resultMap select idselectById resultMapuserResultMap SELECT * FROM user WHERE id #{id} /select常见场景包括数据库字段是下划线风格而Java属性是驼峰风格、查询结果里有聚合函数列名如COUNT(*)需要起别名、多表联查需要把关联对象封装到association或collection里。多表关联映射是resultMap最有价值的场景。比如查询用户并带上用户对应的订单列表resultMap iduserWithOrdersMap typeUser id propertyid columnid/ result propertyuserName columnuser_name/ collection propertyorders ofTypeOrder id propertyorderId columnorder_id/ result propertyproductName columnproduct_name/ /collection /resultMap这里collection映射一对多关系ofType指定集合元素的类型。SQL查询时通过LEFT JOIN把订单数据查出来MyBatis会按id字段作为分界线自动帮你把同一用户的多条记录聚合成一个ListOrder。这个机制底层叫“嵌套结果映射”理解它之后多表联查的XML写起来会非常顺手。3.4 MyBatis缓存机制缓存是性能优化的重要一环也是面试爱问的点。MyBatis有两级缓存一级缓存默认开启范围是SqlSession级别二级缓存默认关闭范围是namespace级别。一级缓存同一个SqlSession对象下连续执行两次相同的查询第二次直接走缓存不会查数据库。但需要注意如果在两次查询之间执行了任何增删改操作commit后缓存就会失效。所以“先查询、再update、再查询”这种操作第二次查询仍然会走数据库。二级缓存在mybatis-config.xml里把cacheEnabled设为true后还需要在具体的Mapper XML里加cache/标签才会启用。二级缓存跨越SqlSession多个SqlSession可以共享同一个namespace下的缓存数据。它的实现原理是把查询结果序列化后存到一个公共区域命中缓存时直接反序列化返回。但在实际项目中我对二级缓存的态度是谨慎非常谨慎。尤其是多表联查的场景如果表A的数据被另一个Mapper更新了而当前namespace的缓存不知道这个变更就有可能读到脏数据。这也是很多团队默认关闭二级缓存的原因。如果你想了解缓存建议在个人项目里做实验生产环境还是优先考虑Redis这类分布式缓存方案可控性高得多。4. 一套可直接复用的Spring Boot MyBatis实操示例4.1 依赖与基础配置理论讲再多不如一套能跑通的代码有说服力。这里给一个从零搭建的完整示例技术栈是Spring Boot 2.7 MyBatis MySQL Maven。先在pom.xml里引入依赖dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency注意spring-boot-starter-parent已经帮你管理了MySQL驱动版本这里不用写version。如果用的是Spring Boot 3.x对应的starter版本建议换到3.0以上。配置文件和前面示例一致重点检查mapper-locations路径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.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl4.2 Mapper接口与XML映射文件定义实体类public class User { private Long id; private String userName; private Integer age; // getter / setter }Mapper接口Mapper public interface UserMapper { User selectById(Long id); ListUser selectList(); int insert(User user); int update(User user); int deleteById(Long id); }对应的XML映射文件放在src/main/resources/mapper/UserMapper.xml?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN https://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.demo.mapper.UserMapper resultMap idbaseResultMap typeUser id propertyid columnid/ result propertyuserName columnuser_name/ result propertyage columnage/ /resultMap select idselectById resultMapbaseResultMap SELECT id, user_name, age FROM user WHERE id #{id} /select select idselectList resultTypeUser SELECT id, user_name, age FROM user /select insert idinsert useGeneratedKeystrue keyPropertyid INSERT INTO user(user_name, age) VALUES(#{userName}, #{age}) /insert update idupdate 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这里有个实用细节insert标签里的useGeneratedKeystrue keyPropertyid可以让数据库自增主键回写到Java对象的id属性上。插入后直接拿user.getId()就能获取新生成的id不用再单独查一次在创建订单、生成关联数据时非常方便。4.3 批量插入与分页查询的常见写法批量操作的效率优化是实际开发的高频需求。逐条insert在数据量大时性能很差一条一条发SQL、一条一条提交事务浪费大量时间在网络上。优先使用批量insert一条SQL搞定insert idbatchInsert INSERT INTO user(user_name, age) VALUES foreach collectionlist itemuser separator, (#{user.userName}, #{user.age}) /foreach /insert批量insert单条SQL拼接的SQL长度要控制好MySQL默认的max_allowed_packet一般是4M或64M如果一次插入5000条数据导致包太大会报PacketTooBigException。我的经验是每批控制在500到1000条具体数值通过压测确定。分页查询在MyBatis原生体系里没有内置支持通常依赖PageHelper插件或者用limit手写。如果项目引入MyBatis-Plus可以用Page对象直接分页。PageHelper的用法很简单如果你不想引插件手写分页SQL也不难select idselectByPage resultTypeUser SELECT id, user_name, age FROM user ORDER BY id DESC LIMIT #{offset}, #{pageSize} /select注意分页参数offset是“从第几条开始”不是页码。调用时要把页码换算成offset(pageNum - 1) * pageSize。同时最好做一个COUNT(*)查询获取总条数否则前端没法渲染总页数。这块逻辑看着简单但直接写LIMIT 10, 20这种写法在数据量很大时性能会下降原因是MySQL要扫描并丢弃前面10条数据。数据量大时可以考虑用“延迟关联”或“游标式分页”来优化不过这就是另一个话题了。5. 常见问题排查与避坑实录下面这些坑是我在实际开发和网友反馈里看到的高频问题干脆整理成一个排查表方便你遇到问题时直接对照。问题现象常见原因解决方案启动报Invalid bound statement (not found)XML文件没被扫描到或Mapper接口和XML的namespace不匹配检查mapper-locations路径确认namespace和Mapper接口全限定名一致查询结果全是null数据库字段下划线与Java驼峰属性没对上没开驼峰映射设置map-underscore-to-camel-case: true或用resultMap手动映射插入中文乱码JDBC URL缺少字符集参数URL加上characterEncodingutf8动态SQL拼接后语法报错if条件判断与实际SQL逻辑冲突比如where位置多出AND用where、set、trim代替手动拼接缓存命中但数据过期二级缓存没清特别是多表join后相关表更新但缓存未失效关闭二级缓存热点数据放Redis批量插入报SQL包过大单批数据量太大超过MySQL的max_allowed_packet分成每批500~1000条SQL日志看不到日志级别太高或没配置log-impl配置log-impl: StdOutImpl或把Mapper包日志级别设为debug自动建表不执行Spring Boot 2.5及以上默认不再自动执行schema.sql配置spring.sql.init.mode: always或改用Flyway这里重点多说一句Invalid bound statement这个报错出现频率极高而且新手很容易手足无措。核心排查路径就三步。第一步检查Mapper接口和XML文件的namespace是否完全一致第二步检查接口里的方法名和XML里的id是否一致第三步检查mapper-locations配置的路径和XML实际存放路径是否能对上。顺序执行基本都能定位到问题。还有一个小问题很考耐心有时候改了XML里的SQL但运行debug看到的还是旧SQL。原因是MyBatis默认不会热加载XML文件需要重启应用或者在IDEA里配合spring-boot-devtools实现重启触发。不要在那里纠结“为什么改了没用”先重启再看大概率就是资源没重新加载。6. 源码视角一次SQL查询在MyBatis内部经历了什么6.1 查询的核心链路如果你把MyBatis看成一个加工厂那一次查询的流程可以概括为读取全局配置、创建工厂、打开会话、获取处理器、执行SQL、映射结果。从源码层面把这条链路打通后你对前面讲的配置项为什么要这样设置会有种“原来如此”的顿悟感。核心链路拆开看是这几步解析配置SqlSessionFactoryBuilder读取mybatis-config.xml或者Spring Boot整合时读取Configuration对象解析出Environment、Settings、MappedStatement等核心对象。MappedStatement就是一个SQL语句的封装体包含SQL的id、参数类型、结果映射等元信息。创建工厂SqlSessionFactoryBuilder.build()返回SqlSessionFactory。打开会话每次调用Mapper方法时通过SqlSessionFactory.openSession()创建SqlSession。获取Mapper代理Mapper接口本身没有实现类MyBatis通过JDK动态代理生成一个代理对象方法调用会转发到MapperProxy再路由到对应的MappedStatement。执行SQLSqlSession把请求交给Executor执行器。Executor有几种类型SimpleExecutor每执行一次SQL就创建一个StatementHandlerReuseExecutor会复用StatementBatchExecutor批量执行SQL。Executor内部会先查一级缓存再决定是否去数据库查询。参数与结果处理StatementHandler用ParameterHandler设置SQL参数执行后ResultSetHandler负责把结果集映射成Java对象。这条链路里拦截器机制也在这里生效。MyBatis允许自定义插件拦截四大核心对象的方法Executor、StatementHandler、ParameterHandler、ResultSetHandler。很多人用的PageHelper分页插件底层就是拦截了Executor的query方法在SQL执行前拼上LIMIT语句。理解这个流程对排查问题很有价值。比如你发现某条SQL执行了两遍那可能是缓存没生效比如你期望走二级缓存但实际没走可能要检查cacheEnabled和namespace配置比如你想打印实际参数值可以在ParameterHandler层面拦截。6.2 关于JDBCType和TypeHandler的一点理解在SQL参数绑定阶段有一个经常被忽略但又很重要的概念JdbcType和TypeHandler。当我们在XML里写#{age}时MyBatis要决定调用PreparedStatement的哪个setXXX方法。它靠的是Java属性类型推断出一个JdbcType然后使用对应的TypeHandler完成类型转换。大多数情况下系统默认的TypeHandler就够用了String对应VARCHARInteger对应INTEGERLocalDateTime对应TIMESTAMP。但如果你用了特殊类型比如数据库的JSON字段存了一个对象字符串Java侧想直接转成对象默认的TypeHandler就不灵了需要自定义一个TypeHandler在setParameter时把对象序列化成JSON字符串在getResult时把JSON字符串反序列化回对象。这个需求在“用MySQL JSON字段存业务扩展信息”的场景下很常见理解了TypeHandler的工作位置也就知道该从哪里扩展。7. 基于这些经验回答面试里最常见的几个问题把前面内容全部消化后面试题基本不用死记硬背顺着逻辑就能答出来。这里挑几个高频问题做串联。MyBatis和JDBC有什么区别JDBC是手动管理连接、手动准备SQL、手动处理结果集代码臃肿且容易出资源泄漏MyBatis在底层封装了这些操作把关注点聚焦到SQL本身和参数、结果的映射上。#{}和${}的区别是什么#{}是预编译占位符走PreparedStatement安全${}是字符串直接替换有注入风险只在表名、排序字段这些无法预编译的场景使用并且值来源要受控。MyBatis的一级缓存和二级缓存是怎么工作的一级缓存是SqlSession级别默认开启同一个SqlSession内相同查询直接命中缓存执行增删改并提交后缓存清除二级缓存是namespace级别需要手动开启跨SqlSession共享但多表联查场景有脏读风险我通常建议谨慎使用。MyBatis和MyBatis-Plus有什么区别MyBatis-Plus在MyBatis基础上增强提供内置CRUD方法、条件构造器、分页插件等能力开发效率高但复杂查询和SQL可控性还是得靠手写XML。两者不冲突很多人实际项目是MP写简单CRUD原生XML写复杂查询。动态SQL有哪些标签if、where、set、trim、choose/when/otherwise、foreach、bind、sql/include每个标签解决一类“SQL片段动态拼接”问题。有没有看过MyBatis源码有。核心链路是SqlSessionFactoryBuilder解析配置构建SqlSessionFactorySqlSession通过JDK代理调用Mapper方法Executor执行SQL并管理缓存内部由StatementHandler、ParameterHandler、ResultSetHandler协作完成SQL语句、参数绑定、结果集映射。拦截器分别在这四个核心对象上扩展实现分页、审计等功能。面试官追问到源码底层时能把这套链路讲清楚基本就过关了。但别止步于背概念最好自己跟着断点走一遍印象会深很多。最后再分享一个我个人的实操体会遇到MyBatis相关的问题不要急着去查业务代码先开启SQL日志打印看清MyBatis最终执行的SQL到底是什么、参数传的是什么、返回的行数是多少。90%的问题光是这一步就能定位到原因。剩下的10%再去看缓存、看数据源配置、看结果映射排查路径清晰效率会提升不少。这套思路我用了很多年希望你也能少走弯路。
网站建设高端定制企业官网