MyBatis初体验:从SQL地狱爬出来的一线生机
今天早上8点刚坐到工位,咖啡还没泡好,钉钉就弹出一条消息:“小张,订单服务那个慢查询问题定位得咋样了?”——这已经是本周第三次被催了。我苦笑一声,心想:要不是两个月前刚跳槽进这家上市公司的技术中台团队,哪会这么快就被扔进“SQL优化火线”里。
说来惭愧,之前在上一家公司主要搞业务逻辑,ORM框架基本靠Spring Data JPA糊弄过去。结果这边一上来就是清一色的MyBatis项目,连个CrudRepository都找不到。被逼无奈,只能连夜啃文档、翻GitLab历史提交记录,甚至偷偷请教隔壁组那位传说中“手写SQL如呼吸般自然”的老哥。
但不得不说,踩过几个坑之后,我对MyBatis真香了。今天这篇笔记,既是给和我一样刚入坑的新人指条明路,也算是在Windsurf(我们内部用的AI代码助手)和DeepSeek(最近狂刷的开源大模型)双重加持下,对持久层框架的一次重新审视。
为什么又是MyBatis?
我们中台团队负责支撑全公司十几个核心业务线的通用能力,比如用户中心、支付网关、风控引擎。这类系统有个共同特点:高并发 + 强一致性 + 复杂查询。JPA那种“全自动ORM”在这种场景下简直是灾难——动不动就N+1查询,JOIN写得像意大利面条,想手动优化?对不起,Hibernate的HQL限制太多。
而MyBatis的核心哲学就一句话:SQL归你,对象映射归我。它不替你写SQL,而是让你用最原始的方式控制数据库交互,同时自动把结果集转成Java对象。这种“半自动”的设计,在需要极致性能调优的场景下简直是救命稻草。
举个真实例子:上周五晚上,订单列表接口TP99飙到2.3秒,DBA直接打爆我的电话。打开Arthas一看,某个LEFT JOIN查了8张表,返回字段上百个,结果前端只用了其中5个。换成MyBatis后,我直接手写一条精简SQL,加了覆盖索引,TP99立马压到180ms——那一刻,我真的想对着屏幕磕一个。
快速上手:三步搞定基础配置
别被网上那些“MyBatis源码解析”吓到,入门其实超简单。以下是我们团队标准化项目的最小可行配置(基于Spring Boot 3.x):
# application.yml
mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.company.platform.order.model
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
对应的Mapper接口长这样:
@Mapper
public interface OrderMapper {
List<Order> selectOrdersByUserId(@Param("userId") Long userId);
int insertOrder(Order order);
}
XML文件则放在resources/mapper/order/OrderMapper.xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.company.platform.order.mapper.OrderMapper">
<select id="selectOrdersByUserId" resultType="Order">
SELECT id, user_id, amount, status, create_time
FROM t_order
WHERE user_id = #{userId}
AND is_deleted = 0
ORDER BY create_time DESC
</select>
<insert id="insertOrder" useGeneratedKeys="true" keyProperty="id">
INSERT INTO t_order (user_id, amount, status)
VALUES (#{userId}, #{amount}, #{status})
</insert>
</mapper>
看到没?SQL完全可见、可控、可调试。不像某些框架把SQL藏在注解里或者动态生成,出了问题连日志都看不懂。
那些年踩过的坑
当然,MyBatis也不是银弹。作为新人,我至少犯过三个经典错误:
1. #{} vs ${}:一个字符引发的SQL注入
刚开始写动态表名时,图省事用了${tableName},结果被安全扫描工具直接标红。后来才知道,#{}会预编译(PreparedStatement),而${}是纯字符串替换——等于敞开大门请黑客进来。
正确姿势:表名、字段名等元数据确实无法预编译,但必须做白名单校验!
if (!ALLOWED_TABLES.contains(tableName)) {
throw new IllegalArgumentException("非法表名");
}
2. 关联查询别乱用resultMap
早期我想偷懒,把订单和用户信息一次性查出来,写了复杂的<resultMap>嵌套。结果发现:只要用户表稍有变更,整个映射就崩了。现在我们团队规范是——单表操作优先,多表关联走Service层组装,除非万不得已才用<association>或<collection>。
3. 批量操作别直接for循环insert
曾经天真地以为:
for (Order order : orders) {
orderMapper.insertOrder(order);
}
就能批量插入……结果线上DB连接池被打爆。正确的做法是用<foreach>:
<insert id="batchInsert">
INSERT INTO t_order (user_id, amount, status)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.userId}, #{item.amount}, #{item.status})
</foreach>
</insert>
注意:MySQL需开启allowMultiQueries=true,PostgreSQL原生支持。
和AI工具的奇妙化学反应
说到这儿,不得不提最近在用的两个“外挂”:DeepSeek 和 Windsurf。
Windsurf 是我们公司自研的AI编程助手,接入了内部代码库和规范文档。当我写完Mapper XML,它能自动提示“该SQL未使用索引”、“建议添加LIMIT防止全表扫描”。
DeepSeek 则是我下班后自己折腾的。它的代码理解能力超强,我随便丢一段复杂SQL过去,它能反向生成对应的MyBatis XML结构,甚至给出分页优化建议。
有一次我卡在一个多条件动态查询上,手写<if>标签写到头秃。把需求描述喂给DeepSeek,30秒后它吐出一套完整的<choose><when><otherwise>模板,还附带单元测试——那一刻,我感觉自己的CRUD生涯被AI点亮了。
性能与运维:生产环境的真实考量
在中台这种高压环境下,光会写MyBatis还不够,还得懂运维。我们总结了几条铁律:
| 维度 | 建议 | 反面案例 |
|---|---|---|
| SQL审计 | 所有Mapper XML需通过SonarQube扫描 | 曾因SELECT *导致网络带宽打满 |
| 慢查询监控 | 接入APM,自动告警超过500ms的SQL | 双11期间漏掉一个未索引查询,损失百万GMV |
| 分页安全 | 禁止物理分页偏移过大(如OFFSET 1000000) | 用户恶意刷页导致DB CPU 100% |
| 缓存策略 | 结合Redis做二级缓存,但注意一致性 | 缓存击穿导致DB雪崩 |
另外,我们强制要求:所有SQL必须有注释说明业务场景。比如:
-- 订单列表页:按用户ID查询近3个月有效订单(用于首页展示)
SELECT ...
这样当半年后你再回来看这段代码,不至于对着屏幕发呆:“这谁写的?啥意思?”
写在最后
入职这两个月,从抗拒MyBatis到主动推广它,我的心态变化很大。以前总觉得“写SQL是DBA的事”,现在才明白:优秀的后端工程师,必须对数据库有敬畏之心。
MyBatis或许不够“现代化”,没有Reactive、没有Fluent API,但它给了我们最珍贵的东西——控制力。在这个AI开始接管编码的时代,掌握底层细节反而成了稀缺能力。
顺便吐槽一句:产品经理昨天又提了个需求,“能不能让用户按任意字段组合排序?”……我默默打开了MyBatis的<trim>标签文档,顺手给DeepSeek发了条消息:“兄弟,救救孩子。”
(完)

评论 0