
这些年做Java后端项目里十套有七八套用的都是MySQL加MyBatis这套组合。说实话MyBatis刚出来那会儿很多人觉得它就是写SQL的工具没什么技术含量但等你真正在业务里跑过几轮就会发现它把SQL的掌控权完全交还给了开发者在复杂查询、报表统计、多表联动这些场景里这种掌控感有多重要。这篇文章不打算复读官方文档而是从我自己项目里的真实经验出发聊聊在Java MySQL这套技术栈下MyBatis到底该怎么快速上手、怎么避坑、怎么把动态SQL和缓存这些能力真正用起来。不管你是刚接触Mapper接口的新手还是已经写过一段时间XML映射文件但总遇到诡异问题的朋友这篇文章应该都能给你一些参考。内容偏实战跟着步骤走基本能直接落到项目里用。1. 为什么是MyBatis而不是别的ORM框架1.1 从JDBC裸写说起早年间我用原生JDBC写过一阵子数据访问层印象太深了。每次查询都要自己管理Connection、PreparedStatement、ResultSet写完查询还要手动关闭资源稍微漏一个close连接池很快就被拖垮。当时一个简单的用户查询方法代码里大半篇幅都在处理数据库连接的获取与释放真正业务逻辑没几行。后来项目里引入了MyBatis这些样板代码基本全部消失连接管理、参数绑定、结果集映射这些脏活累活框架全包了。数据访问层从写JDBC工具类变成了写SQL和结果映射规则开发效率提升的幅度是肉眼可见的。1.2 MyBatis与Hibernate的核心差异不少初学者会在MyBatis和Hibernate之间纠结。Hibernate的理念是全自动ORM你只要定义好实体类和表结构的关系它帮你生成SQL甚至帮你维护表结构。MyBatis的理念则是半自动SQL你自己写框架只负责把参数传进去、把结果集映射成对象。差别在于掌控力。Hibernate自动生成的SQL在简单场景下很好用可一旦遇到多表关联、子查询、复杂条件分支调优就很痛苦因为SQL不是自己写的性能出了问题你得先猜它生成了什么。MyBatis把SQL明明白白摆在你面前慢查询、索引失效、扫描行数过大一眼就能在SQL层面定位。我个人的观点是在业务逻辑复杂、查询路径不可预测的中大型系统里MyBatis这种看得见摸得着的方式更稳。1.3 我在项目里感受到的优势实际项目跑下来MyBatis有几个点是我特别喜欢的。第一是SQL复用能力用SQL片段可以把常用的查询列、where条件抽出来多张Mapper共享改一处全局生效。第二是动态SQL非常灵活if、where、foreach、choose这些标签组合起来几乎能覆盖所有条件拼装场景不用像字符串拼接一样担心SQL注入。第三是性能可控MyBatis对SQL执行过程和缓存都有比较明确的干预入口大促前做压测能清楚知道每个查询是怎么执行的该加缓存加缓存该改SQL改SQL心里有底。对比起来MyBatis就像手动挡的车操作繁琐一点但动力响应直接Hibernate像自动挡省心但急加速时总觉得隔了一层。2. 十分钟搭起第一个可运行的MyBatis项目2.1 环境准备与依赖引入要跑起一个MyBatis项目前提是本地有JDK1.8以上、Maven和MySQL。MySQL安装我建议直接下载官方安装包Windows环境用MySQL InstallerLinux环境用tar包解压或者包管理器安装装完后记得确认3306端口能连通。项目这边我习惯用Maven管理依赖pom.xml里需要引入mybatis核心依赖和MySQL驱动版本号我建议保持相对较新避免老版本对字符集排序规则支持不完善。第一次搭项目时依赖冲突是最常见的坑比如驱动包版本和MySQL服务端版本不匹配会出现Public Key Retrieval is not allowed之类的报错后续排查章节会细说。dependencies dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.16/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency /dependencies2.2 核心配置文件mybatis-config.xml依赖引好后需要一个全局配置文件。它相当于MyBatis的总控室数据库连接、类型别名、驼峰映射、日志输出这些都在这里声明。我自己常用的配置大概长这样?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 environments defaultdevelopment environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/mybatis_demo?useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword valueyour_password/ /dataSource /environment /environments mappers mapper resourcemapper/UserMapper.xml/ /mappers /configuration这段配置里mapUnderscoreToCamelCase是灵魂开启后数据库的user_name字段能自动映射到Java里的userName属性否则你会在实体类或者resultMap里手工写一堆column映射很烦。连接串里的serverTimezone也不能省MySQL 8之后时区问题会直接影响时间字段的读写我之前不加这个参数本地跑得好好的部署到服务器就出现时间偏差八小时的诡异问题排查了半天。2.3 Mapper接口与XML映射文件的绑定MyBatis的设计里Mapper接口和XML映射文件是配对的。接口定义Java方法XML里写对应的SQL和执行规则。很多新手刚接触时搞不懂这个绑定机制这里多说一句MyBatis启动时会把接口的全限定名和XML里的namespace关联起来接口里的方法名就是XML中statement的id。所以XML的namespace一定写成接口全限定名方法id一定要和接口方法名完全一致否则启动直接报错而且这类报错信息还不是很直观。public interface UserMapper { User selectById(Integer id); }mapper namespacecom.demo.mapper.UserMapper select idselectById resultTypecom.demo.entity.User select * from user where id #{id} /select /mapper这里还有一个很容易踩的坑resultType到底是写实体类全限定名还是别名。如果你在全局配置里注册了typeAlias可以写别名否则老老实实写全限定名。我自己一开始图省事写了别名又忘了注册结果启动时直接报Could not resolve type alias后来统一使用全限定名再也没出过这种问题。2.4 参数传递的几种姿势Mapper接口的方法参数传递看起来简单实际用起来的坑也不少。单个基础类型参数XML里直接用#{任意名字}都能取到因为MyBatis会把它当作一个单独的参数处理。多个参数就需要注意了不写Param注解的话XML里只能用param1、param2这类位置命名代码可读性很差我强烈建议多个参数时都加上Param注解显式命名。传对象时直接通过属性名取值。还有一种常见场景是传Map适合字段不固定的情况。我整理了一个简单的对照表参数形式推荐写法XML取值方式单个基础类型selectById(Integer id)#{id}多个基础类型selectByNameAndAge(Param(name) String name, Param(age) Integer age)#{name}、#{age}单个对象insert(User user)#{userName}、#{age}MapselectByMap(Map param)#{keyName}之前我在一个查询接口里同时传了userId和status当时没写Param以为XML里写#{userId}就行结果运行起来一直报There is no getter for property named userId加上Param后秒解决。这个细节在面试题里也经常被问到属于基础但高频的考点。3. 动态SQL与结果映射每天都要用的核心能力3.1 if和where组合查询动态SQL是MyBatis最实用的能力没有之一。最常见的场景就是多条件组合查询用户可能按姓名筛选也可能按状态筛选也可能几个条件一起传。如果用字符串拼接实现不仅要小心翼翼处理多余的where和and还得防SQL注入心累。MyBatis里用if加where标签就能优雅解决select idsearchUsers resultTypecom.demo.entity.User select * from user where if testname ! null and name ! and name like concat(%, #{name}, %) /if if teststatus ! null and status #{status} /if if testminAge ! null and age #{minAge} /if /where order by create_time desc /selectwhere标签会自动处理首个子句前的and或or不会生成where and status 1这种SQL。这里我踩过的一个小坑是test表达式里字符串比较建议写成name不要写成name单双引号在XML和OGNL表达式里处理起来容易出歧义统一用单引号能少很多怪问题。3.2 foreach批量处理批量插入、批量删除是foreach标签的经典应用场景。以前用JDBC写批量插入要么循环执行单条insert数据库连接消耗很大要么手工拼接几百个values代码又丑又容易出错。MyBatis里foreach直接把集合展开到SQL里配合批量提交或批处理模式效率提升明显。insert idbatchInsert parameterTypelist insert into user(name, age) values foreach collectionlist itemitem separator, (#{item.name}, #{item.age}) /foreach /insert使用时有几个小细节要注意。collection属性的值取决于参数类型用Param(list)就写list不写注解直接传List就写collectionlist或collection。item是循环变量名后面取值都用它来引用别在循环体里写成别的名字。separator控制每个元素之间的分隔符批量插入时是逗号批量更新时可能是空字符串配合case when。还有一个常见问题是批量插入的SQL特别长MySQL默认的max_allowed_packet如果太小会直接报错几百上千条数据一起插入时建议把MySQL这个参数调大到16M或者32M。3.3 resultMap解决字段映射最后一公里简单查询用resultType加驼峰映射就够了但多表联查、字段重名、关联对象这些场景resultType就有点顶不住了。这种时候需要resultMap它是MyBatis里最灵活的结果映射手段。举一个例子查询订单表关联用户表我们希望返回的OrderVO里既包含订单信息又包含用户姓名和手机号。resultMap idOrderWithUserMap typecom.demo.vo.OrderVO id propertyid columnorder_id/ result propertyorderNo columnorder_no/ result propertytotalAmount columntotal_amount/ association propertyuser javaTypecom.demo.entity.User id propertyid columnuser_id/ result propertyname columnuser_name/ result propertyphone columnuser_phone/ /association /resultMap select idselectOrderWithUser resultMapOrderWithUserMap select o.id as order_id, o.order_no, o.total_amount, u.id as user_id, u.name as user_name, u.phone as user_phone from orders o left join user u on o.user_id u.id where o.id #{id} /select这种写法有个好处SQL里的别名和resultMap的column一一对应Java对象结构再复杂也能映射得清清楚楚。如果遇到连表查询还叠加动态条件XML会变得很长我的习惯是把公共的SQL片段抽出来下一节就讲讲SQL片段的妙用。3.4 自定义SQL片段复用当某个查询列集合在多个查询里反复出现时比如select详情和select列表都要查同样的几个字段每次都重复写一遍列名很浪费时间。MyBatis的sql标签可以把这一坨提取出来需要的地方include一下。这里有一个小坑include引用的是同一个命名空间里的sql片段如果跨Mapper使用sql标签里要写上完整的refid做限定。我一般建议把通用的列片段放在各自的Mapper里避免跨Mapper引用造成维护混乱。sql片段里还能配合include的property属性传参这招做动态列查询时很实用不过日常项目里简单复用一下就够了别过度设计。4. 缓存机制解析一级缓存和二级缓存4.1 一级缓存作用域与失效情况MyBatis默认开启一级缓存作用域是SqlSession。同一个SqlSession内执行同一条SQL且参数相同第二次查询会直接命中缓存不再访问数据库。这听起来很美好但实际使用有几个坑。第一如果SqlSession对象不是同一个一级缓存就失效Spring整合后每次请求都可能新建SqlSession一级缓存基本指望不上。第二如果两次查询之间执行了增删改操作或者手动调用了clearCache缓存会被清空。第三用了Spring整合之后一级缓存还会和事务绑定事务内同一个SqlSession才能共享事务结束缓存就没了。所以不要对一级缓存抱太多期望它就是MyBatis顺手做的一层内存复用。4.2 二级缓存配置与使用注意二级缓存的作用域是Mapper namespace跨SqlSession共享需要手动开启。配置方式是在全局配置里设置cacheEnabled为true同时在对应Mapper的XML里加一个cache标签。开启后查询结果会以序列化形式存到缓存中第二次查询不走数据库性能提升明显。但副作用也很明显最典型的是脏数据问题。只要这个namespace对应的表被其他途径修改了数据缓存无法感知就会读到陈旧数据。所以我个人的建议是字典表、配置表这种低频变更的数据可以放心开二级缓存高频变更或对数据实时性要求高的业务表别开。还有一个细节开启二级缓存后实体类必须实现Serializable接口否则缓存写入时会报序列化异常我见过有人在这里卡了半天的。4.3 和Spring Boot整合后缓存行为的变化在Spring Boot项目里MyBatis和Spring整合后SqlSession由SqlSessionTemplate管理它会动态代理方法每次方法调用可能获取新的SqlSession所以一级缓存的作用范围进一步缩小基本只存在于事务内。这也解释了为什么很多人在Spring Boot项目里开启了二级缓存查询速度确实变快因为跨方法查询时二级缓存还在生效。我的经验是缓存这块配置宁可少开也不要乱开业务量上来了用Redis做分布式缓存才是正路。MyBatis自带的本地缓存做做单机优化还行多实例部署时一定要关闭或做好同步否则同一份数据在不同机器上的状态不一致出问题很难查。5. 常见问题与排查技巧实录5.1 出参为null但SQL有数据遇到过这种情况吗数据库表里明明有记录接口查出来对象却是null或者某个字段是null。大多数时候是映射问题。第一确认resultType是否写对如果是集合结果却写成了实体类型MyBatis只会取第一条数据封装看起来像数据丢失。第二确认字段映射是否匹配开启驼峰映射后user_name对应userName如果表字段和实体字段命名差异太大得用resultMap或SQL别名处理。第三检查SQL本身执行后在MySQL客户端里能否查到数据有时候是因为表名写错、库选错查了个空表。我在排查时有个习惯先把MyBatis打印的SQL复制到客户端里执行一遍数据层的问题就解决了一半。5.2 自动映射下划线失败这个问题的根源通常是mapUnderscoreToCamelCase没开或者配置没生效。Spring Boot整合下配置文件里可以这样设置mybatis: configuration: map-underscore-to-camel-case: true如果你在XML里同时写了resultMap那么驼峰自动映射不会作用于resultMap里的字段还是得在resultMap中手动写清楚映射关系。我遇到过一种情况某些字段在实体类里是userName数据库里是user_name开了驼峰自动映射后大部分都对了但有个字段还是null排查发现是因为这个字段在resultMap里出现了自动映射被覆盖了。所以规则记住了resultMap里写的字段就必须在resultMap里映射完整用resultType的查询才享受驼峰自动映射。5.3 LocalDateTime类型的处理Java 8的时间类型和MySQL的datetime类型映射在旧版驱动里经常报错或者变成java.sql.Timestamp。解决办法有两个思路。一是在JDBC连接串里配置tinyInt1isBit等参数优化细节时间类型一般不受影响。二是给实体类字段使用LocalDateTime同时把驱动版本升级到8.0.23以上新版驱动对JSR-310支持已经很完整了。如果还不行可以在字段上添加DateTimeFormat或JsonFormat注解配合Jackson处理。这个坑多见于老项目迁移新项目直接从LocalDateTime开始用基本不会有事。5.4 慢SQL定位技巧MyBatis本身不负责SQL性能优化但能帮你快速定位慢SQL。首先把日志级别打开打印出完整SQL和执行参数然后复制到MySQL里用EXPLAIN分析。我看EXPLAIN时主要关注type列如果出现ALL全表扫描那基本就是没有合适的索引优先看where条件和order by涉及字段的索引。还有Extra列出现Using filesort时说明排序没走索引数据量一大性能就会断崖式下跌。实际项目中我经常在Mapper方法名里就标清楚用途比如selectUserByIdWithLock这种后期排查SQL一看方法名就知道哪个语句是核心路径不用一条条翻XML。6. 和Spring Boot、MySQL实际整合的个人配置方案6.1 Spring Boot整合配置现在绝大多数新项目都是Spring Boot MyBatis的组合整合流程比纯MyBatis简单很多。pom里引入mybatis-spring-boot-starter然后配置数据源和MyBatis相关参数就行。约定优于配置很多参数都给了默认值但有几个我习惯显式配置spring: datasource: url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations指定XML文件位置type-aliases-package让XML里可以直接写实体类简单类名map-underscore-to-camel-case处理驼峰映射。第一次整合时忘记写mapper-locations结果启动没报错但调用Mapper方法时一直提示Invalid bound statement其实就是XML没被加载进来。这个报错在MyBatis整合场景下非常高频新手遇到不要慌先检查这个配置。6.2 Mapper接口扫描与事务管理Spring Boot整合后需要在启动类或者配置类上加MapperScan注解指定Mapper接口所在包路径。加了之后框架会动态代理这些接口。也可以在每个Mapper接口上加Mapper注解但接口多了之后很分散我更喜欢集中扫描。事务管理方面在需要事务的Service方法上标注Transactional即可Spring将负责事务的开启、提交和回滚。有个容易忽略的点事务默认只在RuntimeException时回滚如果方法抛出受检异常事务不会回滚。这个行为可以在Transactional的rollbackFor属性里指定。业务代码里抛出异常时我习惯包装成RuntimeException来触发回滚避免事务悄悄地没生效。6.3 分页插件PageHelper的取舍分页查询是Web项目绕不开的需求。PageHelper是我用过最多的MyBatis分页插件用法也简单在查询方法前调用PageHelper.startPage(pageNum, pageSize)后面执行的第一个查询就会被自动拼接limit。用起来很爽但有两个坑必须记住。第一startPage后紧接着的必须是Mapper查询中间不能有其他数据库操作否则分页可能作用到错误的SQL上。第二多数据源或嵌套查询场景下PageHelper可能拦截到意外语句此时建议手动用Interceptor配置或者干脆写limit分页。数据量不大的后台管理系统手写limit一清二楚数据量大或者查询条件复杂的PageHelper确实能省不少事。6.4 控制台打印SQL与参数Debug模式下的一个刚需就是把执行的SQL和参数打印出来。纯MyBatis时通过logImpl配置Spring Boot整合时把log-impl设置为StdOutImpl会在控制台打印完整的SQL预处理语句和参数列表。打印出来的SQL在MySQL客户端里执行时需要手动把参数替换进去注意字符串参数要加引号。另外这些日志在高并发生产环境里会产生大量IO开销我只建议在开发环境和测试环境开启生产环境可以把日志级别调到WARN或者用动态开关配合日志框架管理。我这边的习惯是开发环境输出到控制台测试环境输出到日志文件生产环境默认关闭需要排查问题时再临时开启。7. 一些我认为最重要的实操心得如果现在有人问我刚开始接触Java MySQL下的MyBatis最应该抓住什么我会说是把SQL主动权握在自己手里。MyBatis学习的核心不在于背几个标签而是理解SQL与Java对象之间的映射逻辑理解参数和返回值是怎么流动的。动态SQL和resultMap用熟了复杂查询基本没有障碍再遇到性能问题也能从SQL层面直接优化。还有一个建议是养成看运行日志的习惯。很多诡异问题根本不涉及复杂原理就是SQL拼错了、参数没传对、映射字段写错。把MyBatis的SQL日志打开错误现场基本一目了然。早期我排查一个问题要反复改代码重启后来发现直接看日志就能定位八九成整个排查效率完全不同。最后提醒一句代码规范和命名统一比什么都重要。Mapper接口方法名、XML的id、参数注解这三者之间保持清晰的对应关系项目维护起来会舒服很多。你可以根据自己的偏好决定用注解式SQL还是XML式SQL但在一个项目里尽量保持一致不要今天写注解明天写XML后期维护的人会非常感谢你。这套Java MySQL MyBatis的组合我用了这么多年谈不上有多少惊艳的设计但胜在稳定、直接、可控业务再复杂也能用简单的方式拆解掉。