
先说我自己的经历。刚工作那两年我最怕听到的需求就是把这张表的数据查出来接个列表页面。听起来简单但那时候项目还是老一套 JDBC 手动封装每写一个查询方法都要重复一遍 Connection、PreparedStatement、ResultSet、try-catch-finally 的流程还要自己去拼查询条件、处理 null、做结果集到对象的映射。代码写多了整个人都是麻的。后来项目引入 MyBatis第一次体验到只写一个接口方法加一行 SQL数据就自动变成对象的感觉说实话有点感动。这篇文章就想把 MyBatis 这个东西从头梳理一遍——它是什么、解决了什么问题、核心组件怎么协作、实际项目里怎么跟 Spring Boot、MyBatis-Plus 配合以及那些新手最容易踩的坑。1. 先回答一个问题MyBatis 到底在替我们干什么1.1 那个被 JDBC 折磨的下午如果你没经历过纯 JDBC 开发可能很难理解 MyBatis 的价值。我拿一个最简单的场景举例查用户表里所有状态为 1 的用户。用 JDBC 写大概长这样public ListUser listActiveUsers() { ListUser users new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DriverManager.getConnection(url, username, password); String sql SELECT id, name, age, status FROM user WHERE status ?; ps conn.prepareStatement(sql); ps.setInt(1, 1); rs ps.executeQuery(); while (rs.next()) { User user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setAge(rs.getInt(age)); user.setStatus(rs.getInt(status)); users.add(user); } } catch (SQLException e) { e.printStackTrace(); } finally { // 还得手动关闭而且顺序不能乱 if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (ps ! null) try { ps.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } return users; }这段代码里真正跟业务有关的其实只有一行 SQL 和一个对象赋值循环剩下的全是重复劳动。更麻烦的在于一旦查询条件多了你要么写一堆 if 去拼字符串 SQL要么给每个条件都写一个方法方法数量爆炸式增长。这时候有人会说用 Hibernate 不就行了但 Hibernate 这种全自动 ORM 也有自己的问题复杂查询、动态 SQL、存储过程用起来反而别扭。MyBatis 选择了另一条路SQL 还是你自己写但那些连接管理、参数设置、结果集映射、重复代码统统帮你干掉而且动态 SQL 能力非常灵活。1.2 ORM 阵营里的半自动派很多人把 MyBatis 叫 ORM 框架严格来说它算半自动ORM。全自动 ORM 希望通过对象关系映射让你尽可能不写 SQLMyBatis 的理念则是SQL 是你要掌控的核心资产框架只负责执行和映射。这个理念差异直接决定了它的优劣势优点SQL 完全可控复杂查询、多表关联、数据库特性都能直接写优化空间大SQL 与 Java 代码分离维护方便动态 SQL 比字符串拼接或 Criteria API 更直观。缺点需要自己维护 SQL 和映射关系表结构变动时改动量比全自动 ORM 大对于极简单的 CRUD写 Mapper 接口 XML 反而显得啰嗦。这也是后来 MyBatis-Plus 这类增强框架出现的原因——把单表 CRUD 的模板代码用通用方法替代让你只关心复杂 SQL。一句话总结MyBatis 没打算让你完全脱离 SQL恰恰相反它鼓励你把 SQL 掌握在自己手里。实际开发中这种模式非常顺手因为大部分业务系统的查询逻辑都绕不开动态条件和多表关联与其让框架帮我生成一堆猜不透的 SQL不如自己把 SQL 写得明明白白。2. 一条 SQL 的完整旅程把 MyBatis 核心组件拆开看2.1 SqlSessionFactoryBuilder 与配置文件加载MyBatis 的启动入口是SqlSessionFactoryBuilder。它读取配置文件通常叫mybatis-config.xml构建出全局唯一的SqlSessionFactory。你可以把SqlSessionFactory理解成整个 MyBatis 的总装车间里面装着数据源、事务管理器、映射器注册表等一切运行时需要的物件。核心配置文件的常见结构configuration properties resourcedb.properties/ environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver value${driver}/ property nameurl value${url}/ property nameusername value${username}/ property namepassword value${password}/ /dataSource /environment /environments mappers mapper resourcemapper/UserMapper.xml/ /mappers /configuration这里有一点容易被新手忽略SqlSessionFactory的构建是重量级操作里面涉及 XML 解析、配置校验、Mapper 映射注册、缓存初始化等所以整个应用生命周期里应该只创建一次。很多人第一次写 MyBatis Demo 时在每次调用 DAO 方法前都 new 一个SqlSessionFactoryBuilder去重新构建这个习惯如果带到生产项目里是非常浪费的。后面跟 Spring Boot 集成时这个工厂交给容器单例管理就好。2.2 Mapper 接口和 XML 是怎么绑定的MyBatis 比较特别的一点是Mapper 接口并没有实现类但你可以直接注入接口调用方法。这背后的机制是 JDK 动态代理。启动时MyBatis 会扫描 Mapper 接口为每个接口生成一个代理对象当你调用接口方法时代理会把方法名、参数类型等信息与 XML 里的select、insert等标签的 id 做匹配找到对应的 SQL。绑定规则需要留意XML 文件的namespace必须等于接口的全限定名。XML 中每条语句的id必须等于接口方法名。参数类型、返回类型要能对应上。例如public interface UserMapper { User selectById(Long id); }mapper namespacecom.example.mapper.UserMapper select idselectById resultTypecom.example.entity.User SELECT id, name, age, status FROM user WHERE id #{id} /select /mapper这里#{id}是预编译参数占位符MyBatis 会把它翻译成 JDBC 的?由PreparedStatement执行能有效避免 SQL 注入。如果你用的是${}字符串替换那是直接把值拼进 SQL除非是动态表名、排序字段等必须拼接的场景否则不要用。也有不少人图省事用注解写 SQLSelect(SELECT id, name, age, status FROM user WHERE id #{id}) User selectById(Long id);注解方式在小项目里很爽但一旦 SQL 复杂起来、需要动态判断注解里写script标签会非常难维护。我个人经验是简单查询可以用注解稍微有点动态条件的老老实实放 XML。这也是团队协作里最不容易吵架的方案。2.3 参数传递与结果集映射的幕后逻辑参数映射看起来简单其实有些细节特别影响开发体验。单个基础类型参数时#{任意名字}都能取到多个参数时MyBatis 会把它们包装成一个 Map默认 key 是param1、param2也可以用Param(userId)指定 key。强烈建议多参数方法都加上Param不然别人看代码要猜参数顺序而且一旦改动 SQL排查成本很高。结果集映射有两种常用方式resultType要求数据库列名和实体属性名能对应上。如果列名是下划线风格user_name实体属性是驼峰风格userName需要在配置里打开mapUnderscoreToCamelCase开关。resultMap手动定义列名到属性的映射关系适合多表查询、关联对象嵌套等复杂场景。resultMap才是 MyBatis 真正的杀手锏之一。比如查订单同时带出用户信息你可以用 association 嵌套一个 User 对象查分类带出商品列表用 collection 映射一个 List。早期我见过团队把所有查询都写成 resultType 返回 Map虽然也能跑但类型安全和可读性非常差后来花了很大力气才逐步改成 resultMap。2.4 SqlSession 的生存法则SqlSessionFactory是全局单例SqlSession则是每次数据库会话的轻量级连接替身。它封装了 Connection提供执行 SQL、提交事务、关闭会话的能力。生命周期上有一句口诀SqlSessionFactory常驻SqlSession短命。一个请求进来开一个 SqlSession用完了立刻关闭绝不能把它当成成员变量长期持有。原因很简单SqlSession 底层握着一个数据库连接连接是宝贵资源长期持有等于变相把连接池拖死。以前用原生 MyBatis 时最烦的就是手动管理 SqlSession既要保证 finally 里关闭又要处理事务提交。后来跟 Spring 集成后这些都由框架托管了但理解这个生命周期仍然很重要——很多连接耗尽、事务不生效的问题根源都在这里。3. 从能用到好用Spring Boot 与 MyBatis-Plus 的生态位3.1 为什么说集成方案比 MyBatis 本身更影响开发体验在 MyBatis 官网下载的原始包只是个独立的持久层框架。你得自己创建 SqlSessionFactory、在 DAO 里获取 SqlSession、管理事务这些代码写起来比 JDBC 好不了太多只是核心逻辑简化了。真正让 MyBatis 走进千家万户的是 Spring 集成方案。Spring 集成后你可以直接Autowired注入 Mapper 接口事务交给Transactional声明式管理SqlSession 由 Spring 统一创建和关闭连数据源都交给连接池管理。开发者面对的只剩三件事写接口、写 XML、写业务代码。这里有个概念要分清很多人说Spring Boot 自带 MyBatis这是不对的。Spring Boot 官方其实不维护 MyBatis 的 starter我们常用的mybatis-spring-boot-starter是 MyBatis 团队自己出的第三方 starter只是它的自动配置机制和 Spring Boot 完美契合用起来像官方的一样。3.2 Spring Boot 自动配置替我们省了哪些事引入依赖后Spring Boot 的自动配置会做这些事读取application.yml里的mybatis.*配置项比如 XML 位置、驼峰映射开关、日志实现。自动创建SqlSessionFactory并注册到容器。扫描Mapper注解的接口生成代理 Bean。把SqlSessionTemplate注入容器作为 SqlSession 的线程安全版本供业务使用。所以你在 Spring Boot 项目里只需要写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然后启动类加MapperScan(com.example.mapper)或者在每个 Mapper 接口上加Mapper剩下的就交给框架。这一套组合拳下来MyBatis 的使用门槛比原生时代低了不止一个档次刚入行的同学甚至感觉不到背后还有 SqlSession 这层东西存在。3.3 MyBatis-Plus 补上了什么又带来了什么新问题MyBatis 用久了大家普遍有一个痛点单表 CRUD 的模板 SQL 写起来非常烦。每张表都要写 selectById、selectList、insert、updateById方法长得一模一样纯属体力活。MyBatis-Plus 把这个痛点解决了它提供 BaseMapper 通用方法只要你的实体类配上TableName、TableIdCRUD 直接就有了连 SQL 都不用写。MyBatis-Plus 还带来了条件构造器比如ListUser users userMapper.selectList( new LambdaQueryWrapperUser() .eq(User::getStatus, 1) .like(StringUtils.isNotBlank(name), User::getName, name) );这个 API 写起来比拼 XML 动态 SQL 舒服得多尤其是纯单表查询的简单接口开发效率提升非常明显。所以在springboot mybatis-plus这个组合下很多新项目干脆就不写 XML 了单表操作用 BaseMapper多表操作用 XML 或者注解。但注意MyBatis-Plus 不是银弹。常见问题有两个通用方法的 SQL 是框架生成的遇到带复杂索引的深度分页、多租户、逻辑删除等场景需要花时间理解框架的机制不然性能问题排查起来很被动。MyBatis-Plus 的代码生成器、条件构造器会引入新的 API 学习成本团队如果约定俗成地大量使用QueryWrapper写复杂查询SQL 的可读性会明显下降后期维护不见得比 XML 清晰。我的建议是小型项目、管理后台这类 CRUD 密集的场景放心用 MyBatis-Plus写复杂统计报表、核心交易链路时SQL 还是放 XML 亲自把控。4. 概述之外这几个高频问题最值得提前弄明白4.1 缓存一级缓存与二级缓存以及那些看起来很怪的失效场景MyBatis 的缓存机制几乎每次面试都会被问到原因是被问得多说明框架的使用者里很多人没真正搞懂它。一级缓存是 SqlSession 级别的默认开启。同一个 SqlSession 里执行两条完全相同的 SQL参数也相同第二次查询不会真正走数据库而是直接返回缓存的对象。听起来很美好但要注意一个隐藏问题如果你在同一个 SqlSession 里先查了用户再更新了这条用户数据然后再次查询同一用户MyBatis 为了让数据不过期会把一级缓存清掉下一次查询仍然走数据库。更麻烦的是一级缓存只对当前 SqlSession 生效如果查询之间穿插了任何增删改操作缓存会被清空这也是很多初学者发现缓存好像没生效的原因。二级缓存是 Mapper 级别的跨 SqlSession 共享默认不开启。需要在 XML 里加cache/标签启用。启用之后多个 SqlSession 查询同一 Mapper 下的数据结果会缓存到二级缓存。听起来很好但实际开发中我很少建议开二级缓存原因有两个多表查询的缓存失效很难处理。比如订单查询关联了用户表用户表数据被其他 Mapper 修改后订单 Mapper 的二级缓存根本不知道照样返回旧数据。缓存命中率不稳定反而增加排查问题的难度。业务数据只要改一下就要手工调用flushCache很容易漏。所以我的实践经验是生产环境默认不开二级缓存查询性能问题优先从 SQL 本身和 Redis 这类外部缓存解决别指望 MyBatis 内置缓存兜底。4.2 批量写操作实际开发里真的常用吗怎么用才对使用 MyBatis 进行批量写操作实际开发时这种情况多吗——这个问题几乎每隔一阵子就会有人问因为它直接关系到日常开发体验。先说结论多而且很常见。比如批量导入用户、批量同步订单状态、批量插入日志这些场景在业务后台、数据对账、接口对接里非常普遍。但很多人一上来就写for (User user : userList) { userMapper.insert(user); }这个写法能跑但性能很差。每调一次 insert都走一次完整的网络往返、一次事务操作批量插入 5000 条数据可能要几秒甚至更久。正确的批量写一般有两种方案。方案一XML 里用foreach拼批量 INSERT。insert idbatchInsert INSERT INTO user (name, age, status) VALUES foreach collectionlist itemitem separator, (#{item.name}, #{item.age}, #{item.status}) /foreach /insert这种方式一条 SQL 插入多行数据库执行效率高但要注意 SQL 长度限制一般单条 SQL 控制在几百到一千条以内数据量太大时要分批。方案二使用ExecutorType.BATCH的 SqlSession把多条 SQL 攒在一个批次里执行。这种方式适合跨多表、没法用一条 SQL 搞定的场景。Spring Boot 里要额外配置一个模板不能用平时注入的 SqlSessionTemplate否则执行方式不会变。我的经验是单表单批插入用 foreach 最省心跨表或者 SQL 特别复杂时再考虑 BATCH 执行器。批量写还有一个容易踩的坑MyBatis 的批量插入默认不会把自增主键回填到对象里需要加useGeneratedKeystrue keyPropertyid才能拿到回填主键不然接下来要用 id 做关联时拿到的全是最新一条数据的 id。4.3 单个数字字符比较一个能让你怀疑人生的写法mybatis 单个数字字符比较成为热搜词我一点都不意外因为这个问题实在太多了。现象是这样的有一个动态 SQL要判断某个字符参数是否等于某个数字if teststatus 1 AND status 1 /if这段判断在 MyBatis 里有时候会出人意料地不生效甚至报错。原因在于 OGNL 表达式的类型处理。当参数status是String类型时status 1在 OGNL 里并不是简单的字符串和数字比较它会把两端都转成某种类型去比结果经常是 false导致这个 if 分支根本没进去。解决方案也很简单两种任选if teststatus 1 AND status 1 /if或者if teststatus 1.toString() AND status 1 /if这里有个地方要特别小心外层用双引号、内层用单引号时注意 XML 属性本身已经被双引号包着所以写1不会有冲突但1跟test属性会冲突所以推荐外层单引号内层双引号或者干脆用.toString()。类似的坑在choose、when里也会出现遇到判断不生效时先怀疑类型匹配别急着怀疑数据库。4.4 打印 SQL 的几种姿势从配置到插件调试 MyBatis 时最想看到的就是到底执行了什么 SQL参数是什么。新手最容易的做法是在 XML 里加System.out.println那当然不行正确姿势有好几层。最简单的是配日志实现。在 Spring Boot 的 application.yml 里mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样运行时会直接往控制台打印 SQL 和参数。缺点是 SQL 不带上下文线上环境如果开了这个日志会很乱所以只适合本地调试线上要慎用。更规范的做法是把 Mapper 接口所在包的日志级别调成 DEBUGlogging: level: com.example.mapper: debugMyBatis 会通过 SLF4J 输出 SQL 与参数占位符配合 logback 可以记录到文件线上排查问题时就靠它。另外还有一个很受前端和后端同事欢迎的 IntelliJ IDEA 插件MyBatis Log Plugin。它能把 MyBatis 打印的预编译 SQL 和参数还原成一条可以直接在数据库客户端执行的 SQL复制出来就能跑排查问题特别方便。插件本身没有官方市场版下载需要去 IDEA 插件市场搜或者在 GitHub 上找开源版本安装。这里有个使用经验插件只对本地调试有意义别指望它在生产环境帮你什么。5. 给刚接触 MyBatis 的人学习顺序和面试高频题5.1 建议的学习路线从 XML 到注解再到框架整合我见过不少初学者一上来就学 Spring Boot MyBatis-Plus结果条件构造器用得飞起但连#{}和${}的区别都说不清楚。这种学习路径其实是有隐患的因为你不了解底层机制遇到框架的边界情况时会非常无助。按我的建议学习顺序应该是这样先用原生 MyBatis 写一个不依赖 Spring 的 Demo把 SqlSessionFactory、SqlSession、Mapper XML、resultMap 这些核心概念亲手跑一遍。重点啃动态 SQLif、where、foreach、choose这些标签在工作里用得最多也是 MyBatis 最能帮你省事的地方。理解参数映射和结果集映射特别是 resultMap 的高级用法这决定了你处理多表关联查询时的舒适度。再进入 Spring Boot 集成阶段搞清楚自动配置替代了原来哪些手写代码。最后才是 MyBatis-Plus 这类增强框架把它当作提升开发效率的辅助工具而不是唯一的写法。学习过程中有一个好习惯遇到看不懂的 SQL把 MyBatis 日志打印打开看它对 SQL 到底做了什么改写。比如动态 SQL 拼接后的完整语句、分页插件是如何改写 count 查询的看多了你对框架的理解会远超只会调 API 的人。5.2 面试里绕不开的几个问题MyBatis 相关面试题基本集中在这么几类#{}和${}的区别前者是预编译占位符安全、性能好后者是字符串拼接有注入风险。面试官一般会追问什么场景必须用${}答案是动态表名、排序字段、IN 集合等无法用占位符的情况但必须做好白名单校验。一级缓存和二级缓存机制上面已经详细讲过面试能答出一级缓存是 SqlSession 级、默认开启二级缓存是 Mapper 级、默认关闭、存在脏读风险就够用了。MyBatis 和 Hibernate 的区别核心差异是半自动和全自动回答时最好结合项目经验说明什么场景选什么。Mapper 接口为什么没有实现类也能调用要答到 JDK 动态代理可以顺带说一下 MyBatis 的MapperProxy。MyBatis 如何防止 SQL 注入还是落回到#{}的预编译机制能顺手提一句 PreparedStatement 底层原理就更好了。建议准备面试时不要只背结论要能画着一条 SQL 的执行链路讲接口方法调用到代理代理找 XML 里的语句语句解析参数、执行 SQL、映射结果中间经过哪些组件。把这个链路讲清楚很多面试题其实都串起来了。另外一个值得花时间的方向是读源码。真要说起来MyBatis 的源码在主流框架里算比较薄、比较容易读的。重点关注四个类Configuration、SqlSessionFactory、MapperProxy、ParameterHandler。读源码不是为了面试装逼而是能帮你理解配置项、动态 SQL、缓存的很多奇怪行为。我读完整体的Executor接口和BaseExecutor实现之后以前那些缓存怎么老是失效批量插入为什么不回填主键的疑问基本都自己找到了答案。最后分享一个我自己的习惯从 JDBC 时代一路用过来MyBatis 给我的感觉一直是恰到好处。它不像全自动 ORM 那样想包办一切也没有退回到手写 JDBC 的原始状态而是把数据库访问这件事的重复部分去掉把 SQL 的控制权留给开发者。如果你刚开始学我建议不要急着把 MyBatis-Plus 的通用接口铺满全项目先试着用 XML 写一段时间 SQL把动态 SQL 和 resultMap 练熟。等你能随手写出清晰的嵌套映射、能熟练排查 SQL 执行问题时再用那些增强特性你会发现它们用起来特别顺手因为你已经知道它们内部大概在做些什么了。工具会越来越多但底层那套接口 - SQL - 参数 - 结果映射的链路不会变把这套基础吃透你学任何一个持久层框架都会快很多。