我在县城远程写CRUD,却靠MyBatis活了下来
上周五晚上十点半,我蹲在出租屋的飘窗上啃着泡面,盯着IDE里那堆红得发亮的SQL异常,心里默默问候产品经理祖宗十八代。这已经是本周第三次因为“用户列表加载慢”被拉进紧急会议了。作为一个在上海租房、但精神上还住在老家县城的远程打工人,我其实早就习惯了这种“人在工位,魂在田埂”的生活节奏。
说来惭愧,我这个所谓的“分布式系统研究者”,日常干的最多的事却是写CRUD——Create, Read, Update, Delete。而在这片Java后端的汪洋大海里,MyBatis 就是我赖以生存的救生圈。
为什么不用JPA?因为我穷(时间)
我们项目组去年从单体架构往微服务拆分,领导一拍脑袋:“新服务都用Spring Boot + JPA吧,现代化!”结果上线三天,DBA找上门来:“你们那个UserRepository.findAll()把主库CPU干到90%了。” 原来JPA默认懒加载+自动关联查询,在复杂业务下生成的SQL又臭又长,连索引都救不了。
我翻出压箱底的《MyBatis从入门到精通》(没错,就是那本封面泛黄、页角卷边的实体书),咬牙切齿地重写了DAO层。事实证明,在需要精细控制SQL的场景下,MyBatis比JPA更“省电”——不是省电费,是省数据库资源和线上报警次数。
MyBatis不是ORM,是SQL的高级胶水
很多人误以为MyBatis是ORM框架,其实它更像一个SQL模板引擎。你写SQL,它帮你参数化、映射结果。这种“手动挡”模式反而让我这种小镇做题家如鱼得水——毕竟高考练的就是精准控制。
举个例子,我们有个订单查询接口,需要支持按用户ID、状态、时间范围、商品类目等10多个条件组合筛选。如果用JPA的Specification拼接,代码能绕地球一圈;而MyBatis的<where>标签配合<if>判断,干净利落:
<select id="queryOrders" resultType="Order">
SELECT id, user_id, status, amount, create_time
FROM orders
<where>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND create_time >= #{startTime}
</if>
<!-- 更多条件... -->
</where>
ORDER BY create_time DESC
</select>
这种写法不仅性能可控(每条SQL我都亲手优化过执行计划),而且调试时直接复制到Navicat就能跑,运维同学再也不用半夜打电话问我“你这接口到底查了啥表”。
性能优化:别让MyBatis变成“慢”Batis
刚用MyBatis时我也踩过坑。有一次为了图省事,用resultType="map"返回所有字段,结果前端只用了3个字段,其余20多个大文本字段全从数据库拖出来,带宽直接爆表。后来学乖了,严格定义ResultMap,只查需要的列:
<resultMap id="OrderSummaryMap" type="OrderSummary">
<id property="id" column="id"/>
<result property="userId" column="user_id"/>
<result property="amount" column="amount"/>
</resultMap>
另外,批量操作一定要用ExecutorType.BATCH。之前有同事写了个循环逐条插入日志,10万条数据跑了20分钟。我改用MyBatis的批量插入,配合rewriteBatchedStatements=true(MySQL驱动参数),5秒搞定。这种优化在双11大促前特别救命。
| 操作方式 | 1万条耗时 | CPU占用 | 网络IO |
|---|---|---|---|
| 单条循环插入 | 120s | 高 | 极高 |
| MyBatis批量 + rewriteBatchedStatements | 4.8s | 中 | 低 |
和Spring Boot的甜蜜联姻
现在谁还单独用MyBatis啊?基本都是mybatis-spring-boot-starter一把梭。配置简单到离谱:
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开发时看SQL超爽
不过要注意,Mapper接口必须加@Mapper注解,或者在启动类加@MapperScan。我第一次忘加,报错Invalid bound statement (not found),查了两小时,最后发现是包扫描没配——这种低级错误,只有远程办公、没人搭理你的时候才会犯。
为什么我不转Go?
最近团队有人鼓吹“Go才是未来,Java太重了”。确实,Go写API快,部署轻,但我反问一句:你们Go项目的SQL谁写?GORM?那玩意儿在复杂查询面前还不如MyBatis灵活。而且我们这套基于MyBatis的DAO层已经沉淀了三年,各种分页插件、多数据源、读写分离方案都稳如老狗。重构?除非老板给我三个月带薪摸鱼。
再说,作为一个靠ChatGPT辅助开发的“AI民工”,我用Claude帮我生成MyBatis XML模板的速度,比手写Go struct快多了。真香!
最后一点真心话
有人说MyBatis过时了,该用JOOQ、QueryDSL这些DSL工具了。但我想说,在县城远程办公的现实里,稳定、可控、易调试的技术栈才是王道。我不是不想追新,而是每次新框架上线,测试同学都要多写200个Case,运维要重新适配监控告警,产品经理又要延期交付——最后锅还是我背。
所以,与其盲目追求技术时髦,不如把MyBatis用到极致。比如我们最近在搞二级缓存(虽然官方不推荐),用Redis做缓存穿透保护;再比如自定义TypeHandler处理JSON字段,避免业务层反复解析。
回到开头那个泡面夜晚,我最终通过给user_id加复合索引 + MyBatis的fetchSize调优,把接口响应从3s降到200ms。提交代码后,我关掉电脑,望向窗外陆家嘴的霓虹——那一刻,我觉得自己虽然是个小镇做题家,但在代码世界里,也算得上是个“性能优化师”了。
对了,如果你也在用MyBatis,推荐你买本纸质书放在桌上。不是为了装逼,而是当你被线上问题逼到崩溃时,翻开书页的沙沙声,比任何AI提示都更能让你冷静下来。

评论 0