尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Spring Boot 为何保留 MyBatis?SQL 控制力与实战细节全解析
1. 为什么到了 Spring Boot 时代我还是把 MyBatis 留在技术栈里前阵子帮一个朋友团队做代码评审他们的项目是 Spring Boot 起步持久层选了 JPA。刚开始业务简单写起来确实快实体类一标注解增删改查基本不用碰 SQL。半年之后业务复杂起来问题就一个个冒出来了多表关联查询走了别扭的 JPQL懒加载在事务外面触发了一堆 N1为了压接口性能不得不写原生 SQL还得想办法绕开 JPA 那一层封装。最后他们花了两周把核心查询全部重写成 MyBatis才把接口响应时间拉回来。这件事我印象很深。Spring Boot 和 MyBatis 这个组合在不少新团队眼里算老派总觉得现在都在聊 Spring Data JPA怎么还有人回去写 XML。但我在一线写了这么多年后端见得最多的反而是用了 JPA 之后管不住 SQL绕了一大圈又回到 MyBatis。所以这篇就从一个实际干活的人的角度聊聊 Spring Boot 跟 MyBatis 到底怎么搭、配置怎么设、日常开发和排坑时有哪些值得注意的细节尽量让刚接触这个组合的人也能少走弯路。1.1 跟 JPA/Hibernate 比起来MyBatis 赢在控制力很多人喜欢用一张大表格比较 JPA 和 MyBatis我不太爱这么干。框架选型从来不是谁比谁高级的问题而是控制力放在哪一层的问题。对比维度MyBatisSpring Data JPASQL 控制力完全手写SQL 长什么样你说了算框架生成复杂查询要绕 JPQL 或原生 SQL学习曲线会 SQL 就会用概念少要先理解实体状态、懒加载、缓存等概念复杂动态查询一个where加几个if就完事要组合 Specification / QueryDSL心智成本高性能排查直接看 SQL、改 SQL先要搞清楚框架生成的 SQL 对不对再改常规 CRUD手写 mapper稍微费点事几乎零代码继承接口就行我的看法很朴素如果你的业务模型确实复杂领域逻辑多于查询逻辑JPA 的实体建模能力是实打实的好东西但如果你做的是典型的企业服务、报表系统或者商城后台查询条件翻来覆去变SQL 本身才是核心资产。这时候把 SQL 交给一个黑盒去生成等于把最关键的调优空间让了出去。MyBatis 的本质就是SQL 是第一公民这句话不是夸它先进而是说它把决定权还给了你。1.2 什么样的项目适合这个组合以我的经验下面几类项目用 Spring Boot MyBatis 非常顺手查询为重、报表为辅的系统比如运营后台、财务统计、订单处理SQL 复杂度高且经常需要手写调优团队里有专门负责数据库的人或者开发本身 SQL 功底扎实想保留对索引、执行计划的直接控制老系统迁移已有大量 SQL 资产用 MyBatis 搬过来成本最低需要跟 MySQL 的分库分表、读写分离等中间件配合的场景手写 SQL 更容易对接。反过来如果项目本质是围绕领域模型做业务编排实体关系复杂、状态流转多我建议你认真考虑 JPA 或者干脆用 MyBatis 但只做简单的数据访问别硬上复杂的 ORM 映射。工具没有好坏只有合不合适。2. 初始化工程依赖、配置与目录约定2.1 依赖选型官方 starter 与 MyBatis-PlusSpring Boot 项目里集成 MyBatis最常见的是用官方提供的mybatis-spring-boot-starter。以 Spring Boot 2.x 为例常见写法是这样的dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency如果项目是 Spring Boot 3.x要选对应配套的mybatis-spring-boot-starter3.x 版本这一点很容易踩坑拿 2.x 的 starter 硬凑到 Spring Boot 3 上启动时大概率会遇到一堆兼容性报错。至于 MyBatis-Plus我的建议是团队小、追求开发效率可以直接上 Plus它把单表 CRUD、分页、逻辑删除都封装好了确实省事。但要知道 Plus 是在 MyBatis 之上做增强遇到复杂 SQL 时你仍然要回到 XML 去手写。所以不管用不用 Plus基础配置、SQL 映射、动态 SQL 这套底层能力你是绕不开的这篇文章先把这些讲透。2.2 application.yml 里的配置项到底在配什么很多人配置 MyBatis 就是网上复制一段 yaml出了问题根本不知道是哪个配置项引起的。下面这段是我常用的最小配置逐行拆开看spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: truemapper-locations告诉 MyBatis 去哪里找 XML 映射文件这里写的是 classpath 下的mapper目录。注意这个路径一定要跟你的实际目录一致否则启动时报Invalid bound statement (not found)的错误。type-aliases-package实体类别名包路径。配了它之后XML 里写resultTypecom.example.demo.entity.User可以直接简写成resultTypeUser。不配也不影响功能只是 XML 里全限定名很长。map-underscore-to-camel-case开启下划线转驼峰。数据库字段created_at自动映射到实体属性createdAt这是最值得开的一个配置能省掉很多手动resultMap。还有一个我在排查问题时会开的开关configuration.log-impl。它能让你在控制台直接看到 MyBatis 打印的 SQL 和参数排查问题极其有用上线前记得关掉。mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl2.3 Mapper 接口与 XML 文件的对应关系Spring Boot 里用 MyBatis基本套路是一个 Mapper 接口对应一个 XML 文件。接口负责方法声明XML 负责 SQL 实现。Mapper public interface UserMapper { User findById(Param(id) Long id); ListUser search(Param(name) String name, Param(status) Integer status); }对应的 XML 这样写mapper namespacecom.example.demo.mapper.UserMapper select idfindById resultTypeUser SELECT id, user_name, status, created_at FROM user WHERE id #{id} /select /mapper有几个硬性约定必须记住XML 文件的namespace必须等于接口的全限定名select标签的id必须等于接口方法名接口方法的参数多个时必须用Param指定名字XML 里才能用#{name}引用resultType可以写别名但前提是type-aliases-package配置正确。接口上除了加Mapper注解也可以在启动类上加MapperScan(com.example.demo.mapper)批量扫描。我个人的偏好是启动类统一MapperScan这样每个接口不用重复写注解目录结构也更清晰。但有些团队坚持每个接口显式Mapper各有各的道理二选一就行别混用。3. 日常增删改查动态 SQL 和结果映射的边界细节3.1 动态 SQL 里 if 判断的常见坑业务系统里 90% 的查询都是条件查询MyBatis 的if是使用频率最高的标签也是最容易写错的地方。常见需求按名字模糊查按状态精确查条件是可选参数。select idsearch resultTypeUser SELECT id, user_name, status, created_at FROM user where if testname ! null and name ! AND user_name LIKE CONCAT(%, #{name}, %) /if if teststatus ! null AND status #{status} /if /where ORDER BY created_at DESC /select这里两个点要注意一是where标签会自动处理掉第一个条件前面的AND如果不小心写了WHERE 11再拼条件的旧写法建议改掉二是#{name}用在 LIKE 子句里时MySQL 环境下我常用CONCAT(%, #{name}, %)而不是在 Java 代码里拼好%xxx%再传进来后者容易在后续接口复用或者做 SQL 审计时引出一堆麻烦。还有一个高频错误对数字类型做if teststatus ! 。status是Integer时判断! 永远成立因为空字符串跟整数压根不相等。判断数字参数只写status ! null就行了这是很多新人排查一整天也发现不了的问题。3.2 ResultMap 与自动映射什么时候必须手动写开启了map-underscore-to-camel-case后单表查询绝大多数情况下不需要手动写resultMapMyBatis 会按字段名自动映射。但遇到下面这些场景就必须手动处理了多表联查返回的是多个实体的组合字段一对多关联一查订单时要把订单项也带出来实体里面有复合属性比如地址对象、枚举对象。一对多场景最经典比如查用户时把每个用户的订单列表也带上resultMap iduserWithOrdersMap typeUser id propertyid columnid/ result propertyuserName columnuser_name/ collection propertyorders ofTypeOrder id propertyid columnorder_id/ result propertyorderNo columnorder_no/ /collection /resultMap注意这里的column是 SQL 查询结果里的列名所以联表查询时最好给重名字段起别名比如o.id AS order_id。id标签不是随便写的它告诉 MyBatis 哪些行属于同一个对象如果不写集合合并逻辑可能会错乱。我的经验是能不用collection就不用。它本质上是在应用层帮你做了一次内存拼接数据量大时性能并不理想。商详页或者报表场景更稳妥的做法是分开查询查一次用户、按用户 id 批量查订单再用 Java 代码组装。别被优雅映射迷惑能用两次查询解决的事别让一个collection把 SQL 变成笛卡尔积。3.3 主键回填与批量插入插入数据后要拿自增主键是另一个常见需求。MyBatis 在 XML 里这样写insert idinsertUser useGeneratedKeystrue keyPropertyid INSERT INTO user(user_name, status) VALUES (#{userName}, #{status}) /insert记住keyProperty填的是实体属性名不是数据库字段名。插入完成后user.getId()就能拿到数据库分配的自增 id不需要额外查询。如果数据库用的是雪花算法之类应用层生成主键就在 insert 前把 id 赋给实体XML 里直接写#{id}就好。批量插入的正确姿势insert idbatchInsert parameterTypelist INSERT INTO user(user_name, status) VALUES foreach collectionlist itemitem separator, (#{item.userName}, #{item.status}) /foreach /insertforeach的collection名字接口方法参数是List且没加Param时默认叫list加了Param(userList)就写userList。建议统一加Param避免两个程序员理解不一致。批量插入一次别塞太多几百条一个批次比较稳妥堆到几千条SQL 报文过大MySQL 会直接在性能上给你脸色看。4. 分页、连表与慢查询性能优化的一次完整复盘4.1 PageHelper 分页原理与失效场景MyBatis 分页最常用的是 PageHelper用法简单到让人容易忽略它的原理PageHelper.startPage(pageNum, pageSize); ListUser users userMapper.search(name, status); PageInfoUser pageInfo new PageInfo(users);注意一个铁律PageHelper.startPage之后必须紧跟一条 Mapper 查询中间不能有别的 SQL 操作。我见过一个同事在startPage和查询之间加了一段日志查询代码结果日志那条查询被拦截做了分页真正的业务查询没分页数据全部返回接口直接超时。再深入一点PageHelper 的拦截器是基于执行器拦截实现的它会把你的 SQL 包一层 count 查询再包一层 limit。但遇到union查询、带有for update的查询、group by且聚合字段很复杂的场景自动生成 count 的 SQL 很可能不是你想要的。我的处理习惯是分页查询尽量单独写专用 SQL复杂报表就别硬套 PageHelper自己写LIMIT #{offset}, #{pageSize}更可控。4.2 一对多查询的 N1 问题排查N1 这个问题用 JPA 会踩用 MyBatis 也一样会踩只是踩法不同。最常见的就是循环里查数据库for (Order order : orderList) { ListOrderItem items orderItemMapper.findByOrderId(order.getId()); order.setItems(items); }表面上逻辑没毛病但orderList有 100 条就额外发了 100 次查询。数据量小没事到几千条的时候接口基本就废了。这类问题的解法是用空间换查询次数先把订单 id 收集成列表一次WHERE order_id IN (...)查出所有订单项再在内存里按订单分组。ListLong orderIds orderList.stream().map(Order::getId).toList(); ListOrderItem items orderItemMapper.findByOrderIds(orderIds); MapLong, ListOrderItem itemMap items.stream() .collect(Collectors.groupingBy(OrderItem::getOrderId)); orderList.forEach(order - order.setItems(itemMap.getOrDefault(order.getId(), Collections.emptyList())));这种写法第一次看觉得绕但它是所有持久层框架都在背后偷偷干的事。手动写出来至少你能看见、能控制。4.3 从 SQL 日志里定位慢查询排查性能问题第一步永远是看清楚到底执行了什么 SQL。刚才说过的StdOutImpl日志开关就派上用场了。控制台输出长这样 Preparing: SELECT id, user_name, status, created_at FROM user WHERE status ? Parameters: 1(Integer) Total: 86重点看两个地方一是Preparing那行的 SQL 有没有用到索引二是Total返回行数是否符合预期。如果 SQL 没问题但还是很慢就直接把这条语句拿到数据库客户端里EXPLAIN一下看看索引命中情况别在应用层瞎猜。我个人的定位顺序是先确认慢在哪条 SQL再确认这条 SQL 的执行计划最后才是考虑加缓存、加索引。很多人一上来就 Redis 缓存怼上掩盖了问题本身等缓存穿透的时候会一次性加倍还回来。5. 踩坑实录转义、驼峰与多数据源5.1 XML 里的特殊字符和 #{} 与 ${} 的边界XML 映射文件本质是 XML 文档SQL 里的、符号如果不转义文件直接解析失败。比如查创建时间早于某时间点WHERE created_at #{startTime}这段代码在 XML 里会直接报错必须写成lt;或者用![CDATA[ ]]包住。我比较喜欢用 CDATA可读性更好select idfindBeforeTime resultTypeUser ![CDATA[ SELECT id, user_name, status, created_at FROM user WHERE created_at #{startTime} ]] /select另一个绕不开的话题是#{}和${}。#{}是预编译参数占位符会在执行时变成?由 JDBC 处理安全且能防止注入${}是直接拼接字符串等于把参数原样拼进 SQL多用于动态表名、动态排序字段。能用#{}就绝不用${}这是硬底线。如果实在要用${}做排序字段一定在 Java 侧先做白名单校验别让用户传什么拼什么。5.2 驼峰映射为什么字段名总对不上开了map-underscore-to-camel-case不代表万事大吉。这个配置只对数据库下划线字段转 Java 驼峰属性有效反过来查询结果里如果有COUNT(*)、SUM(amount)这类没有别名、没有下划线的列名时映射就会失效。select idcountByStatus resultTypeUser SELECT status, COUNT(*) AS cnt FROM user GROUP BY status /select这里cnt想映射到实体里的count属性光靠自动映射不行要么在实体里正好有cnt属性要么显式写resultMap指定columncnt propertycount。所以我做统计查询时习惯给所有聚合函数列都起一个清晰的别名并且保证实体里有对应属性清爽直接。5.3 多数据源踩过的一次真实教训最后聊多数据源。集成多数据源时如果直接配两个DataSource而不指定主次Spring Boot 启动时会因为存在多个DataSource候选对象而报错。常见做法是用Primary标记主库再通过 AOP 或分包方式切换数据源。我踩过最深的一个坑是这样的项目里配了主库和报表库两个数据源结果select全走了主库报表库的select也走了主库因为事务管理器默认绑定了Primary数据源而我在Service层加了Transactional注解事务一开启连接就固定到主库了。当时的解决办法是不同的Mapper接口包区分存放分别配置不同的SqlSessionFactory和MapperScan并且在读报表库的 Service 上不套Transactional或者单独配置对应的事务管理器。这类问题排查起来很费劲因为代码看起来完全正常问题出在框架的默认行为上。如果你的项目复杂度不高我的建议是多数情况下一个数据源够了别为了看起来高端引入多数据源。多数据源带来的连接管理、事务边界、事务管理器选择每一样都要额外的心智成本收益却未必对等。我用 Spring Boot 加 MyBatis 做了好几个完整项目之后最深的感受是这个组合不会替你做决定但会把所有决定权都交给你。SQL 写得烂它如实执行SQL 写得好它也不拖后腿。上面这些细节都是我在一个个半夜排查问题、一遍遍看执行计划的过程中攒下来的。如果你正在搭新项目或者准备从别的 ORM 迁过来照着这些配置和习惯来应该能少走不少弯路。
RELATED

相关推荐

Android声道数解析:AudioFormat.getChannelCount()与设备适配实战

Android声道数解析:AudioFormat.getChannelCount()与设备适配实战

我上个月在排查一个播放卡顿问题,把声道数相关代码翻了个底朝天。发出去的音频流格式明明写着CHANNEL_OUT_STEREO,AudioFormat.getChannelCount()也返回了 2,可插上某个 USB 声卡后声音就是不对。纠结了两天,最后发现不是驱动问题…

📅 2026/10/15 23:26:14
Django+微信小程序实现导师双选:状态管理与并发控制实战

Django+微信小程序实现导师双选:状态管理与并发控制实战

1. 导师双选,到底难在哪:从"两张Excel表"到"抢人现场"的升级每年毕业设计季,很多高校都在做同一件事:把几百个学生分配给几十位导师。表面上看,这不就是发个通知、填个表、按成绩排个序吗&#xf…

📅 2026/10/15 23:26:14
Java固定资产管理系统源码解析与二次开发实战指南

Java固定资产管理系统源码解析与二次开发实战指南

简介:这份资源是面向计算机专业学生与Java初学者的一站式企业固定资产管理系统项目资料,围绕Java与SQL数据库技术,解决课程设计、毕业设计与答辩全流程的素材需求。压缩包共1.68MB,虽文件总数暂未统计,但涵盖论文、开题…

📅 2026/10/15 23:26:14
MORE NEWS

更多资讯

📰

C#实现平面直角坐标与极坐标转换:从数学原理到工程实践

1. 坐标系转换到底在解决什么问题平面直角坐标和极坐标,本质上是用两套完全不同的语言在描述同一个点。直角坐标说的是“你往东走3公里,再往北走4公里”,极坐标说的是“你朝东北方向走5公里”。两种说法指向同一个位置,但适用的场…

📰

Python办公文档自动化:Word与Excel创建解析实战

1. 为什么办公文档自动化是绕不开的一课做 Python 自动化的人,早晚会撞上 Office 文档这堵墙。你可能一开始只是想批量改几个 Word 里的日期,或者从一堆 Excel 里把某几列数据抠出来汇总,结果搜着搜着就掉进了python-docx、openpyxl、xlrd、p…

📰

【Cursor文档】Cursor中文官方文档教程:从VS Code迁移到AI代码库索引的完整配置指南(含TaoToken统一Key接入)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

AI 领域联邦学习通信效率优化的创新之路:TaoToken 统一 Key 通道下的参数同步调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

当你用来做自动化测试编排的Openclaw崩了时,对走在AI赋能软件测试前排的你有什么启事?TaoToken统一Key通道的排查清单

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Cocos2d-x 3.0源码精读:从Ref内存管理到Renderer渲染管线

1. 这不是一本普通的技术书评,而是一份源码级的Cocos2d-x认知地图“结合源码看《我所理解的cocos2dx-3.0》—— 概述”这个标题里藏着三重信息:它不是泛泛而谈的读书笔记,而是以源码为锚点的深度解构;它聚焦的不是整个Cocos生态&a…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬