MyBatis入门:一个Vim党在试用期的生存实录
上周五晚上十点半,我盯着屏幕上那行 org.apache.ibatis.binding.BindingException: Invalid bound statement (not found),差点把键盘扔出窗外。作为一个刚入职两周、还在试用期的前端转全栈新人,被安排重构一个老后台服务不说,还非得让我用 MyBatis —— 我连 IDEA 都没装,日常只靠 Vim + 终端混日子。
更离谱的是,这还是个“综合业务系统”,涉及用户、订单、库存、日志……数据表之间关系错综复杂,产品经理昨天还发来新需求:“能不能加个实时推荐功能?用点算法优化下用户体验。” 我心里一万个问号:算法?在 Java 后端用 MyBatis 写个 SQL 就算“算法”了?
但没办法,试用期嘛,老板说“多学点东西对你有好处”。于是,我在上海租的 15 平米小屋里,泡了第三杯速溶咖啡,决定从零搞懂 MyBatis。这篇笔记,既是记录,也是自救。
为什么是 MyBatis?
我们组后端技术栈比较“复古”——Spring Boot + MyBatis + MySQL。虽然现在很多人吹 JPA、Hibernate 自动化 ORM 多香,但我们老大坚持用 MyBatis,理由很朴素:“SQL 我们要看得见、改得了、调得动。”
这话我其实挺认同的。尤其在我调试一个慢查询时,发现某条 JOIN 语句没走索引,直接导致接口响应时间从 200ms 飙到 2s。如果是 Hibernate,我可能得翻半天日志猜它生成了啥 SQL;而 MyBatis 的 XML 映射文件里,SQL 就明明白白写在那儿,改起来贼快。
不过,对刚从 Vue 动画世界跳过来的我来说,MyBatis 的配置确实有点劝退。什么 SqlSessionFactory、MapperScannerConfigurer、@MapperScan……光是 Spring Boot 整合就让我绕了三天。
从“Hello World”开始:别信网上的过时教程
很多博客还在教你怎么手动创建 SqlSession,然后 .selectOne()。拜托,2024 年了,咱都用 Spring Boot 自动装配好吗?
正确的姿势应该是:
- 引入依赖(Maven):
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</vaersion>
</dependency>
- 配置
application.yml:
spring:
datasource:
url: jdbc:mysql://localhost:3306/demo_db?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.entity
configuration:
map-underscore-to-camel-case: true # 数据库下划线自动转 Java 驼峰
- 定义实体类(Entity):
public class User {
private Long id;
private String userName;
private String email;
// getter / setter 省略
}
- 写 Mapper 接口(注意:不用实现!):
@Mapper
public interface UserMapper {
User selectById(Long id);
}
- 写对应的 XML 文件(
resources/mapper/UserMapper.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.example.demo.mapper.UserMapper">
<select id="selectById" resultType="User" parameterType="Long">
SELECT id, user_name, email FROM users WHERE id = #{id}
</select>
</mapper>
搞定!启动项目,注入 UserMapper 直接调用方法就行。重点来了:namespace 必须和接口全路径一致,id 必须和方法名一致,否则就是开头那个 BindingException —— 我就是因为手滑把 selectById 写成 selectbyId(小写 b),debug 到凌晨两点。
动态 SQL:比前端模板还灵活?
以前做前端动画,经常用 JS 拼字符串控制 DOM 行为。没想到在后端,MyBatis 的动态 SQL 也这么“拼”!
比如,产品要求用户列表支持按姓名、邮箱、状态多重条件筛选。传统做法是写一堆 if-else 拼 SQL,容易出错还难维护。MyBatis 提供了 <where>、<if>、<foreach> 等标签,简直像 Vue 的 v-if 一样丝滑:
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="email != null and email != ''">
AND email = #{email}
</if>
<if test="statusList != null and statusList.size() > 0">
AND status IN
<foreach collection="statusList" item="status" open="(" separator="," close=")">
#{status}
</foreach>
</if>
</where>
</select>
这段代码会自动处理 WHERE 后面的 AND 多余问题(比如第一个条件为空时不会多出 AND),还能安全地遍历列表参数。注意:collection 属性要和方法参数名或 @Param 注解一致,否则又报错!
性能与安全:别让 SQL 成为你的定时炸弹
作为前端出身,我对“注入攻击”一直很敏感。好消息是,MyBatis 默认使用 #{} 占位符,底层是 PreparedStatement,天然防 SQL 注入。但如果你手贱用了 ${},那就等于裸奔:
<!-- 千万别这么干! -->
SELECT * FROM users WHERE name = '${userName}'
一旦 userName 被传入 ' OR '1'='1,整个用户表就暴露了。所以团队规范里明确禁止使用 ${},除非你 100% 确定内容安全(比如表名动态切换,但也得加白名单校验)。
另外,关于性能:
避免 N+1 查询:比如查订单时关联查用户信息,如果用
select * from orders+ 循环查userMapper.selectById(order.userId),那就是灾难。正确做法是用<resultMap>做关联映射,或者干脆分两次查再内存合并。合理使用缓存:MyBatis 有一级(SqlSession 级)和二级(Mapper 级)缓存。但二级缓存在分布式环境下容易脏读,我们线上基本关掉,靠 Redis 做应用层缓存。
批量操作:插入大量数据时,别一条条
insert,用<foreach>批量插入:<insert id="batchInsert"> INSERT INTO users (user_name, email) VALUES <foreach collection="list" item="user" separator=","> (#{user.userName}, #{user.email}) </foreach> </insert>
和“算法”有啥关系?
回到产品经理提的那个“算法”需求。其实他想要的是:根据用户历史行为,推荐可能感兴趣的商品。这听起来高大上,但落地到 MyBatis 层,无非是写几条聚合查询:
-- 找出用户最近点击最多的品类
SELECT category_id, COUNT(*) as cnt
FROM user_click_log
WHERE user_id = #{userId}
GROUP BY category_id
ORDER BY cnt DESC
LIMIT 5;
真正的“算法”逻辑其实在 Service 层用 Java 实现(比如加权评分、协同过滤简化版),而 MyBatis 只负责高效、安全地把数据捞出来。所以说,在企业级应用中,“算法”的落地往往依赖于底层数据访问的稳定与性能——这也正是 MyBatis 这类框架的价值所在。
试用期感悟:工具不重要,解决问题才重要
说实话,我一开始特别抵触 MyBatis,觉得 XML 配置太啰嗦,不如 JPA “一行注解搞定”。但真正用起来才发现,可控性才是生产环境的生命线。特别是在我们这种“综合业务系统”里,SQL 调优、慢查询分析、分库分表预埋,都需要对 SQL 有绝对掌控力。
而且,MyBatis 和 Java 结合得很自然。你可以轻松地在 Mapper 方法里传复杂对象、返回 Map、甚至调用自定义 TypeHandler 处理 JSON 字段。这种灵活性,对于快速迭代的创业团队(没错,我们公司去年刚融 A 轮)来说,简直是刚需。
至于 IDE?我现在还是用 Vim 写 XML,靠 coc.nvim 补全标签,mybatis-log-plugin 看执行 SQL。虽然同事笑我“复古”,但只要代码跑得稳,谁管你用啥编辑器呢?
最后的小贴士(血泪经验)
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
BindingException |
Mapper 接口方法名 ≠ XML id | 仔细核对大小写、拼写 |
| 返回 null 或空集合 | 数据库字段名 ≠ Java 属性名 | 开启 map-underscore-to-camel-case 或用 resultMap 显式映射 |
| 事务不生效 | 方法非 public / 未加 @Transactional |
确保方法 public 且在 Spring 管理的 Bean 中 |
| 插入后拿不到 ID | 未配置 useGeneratedKeys |
在 <insert> 标签加 useGeneratedKeys="true" keyProperty="id" |
今天终于把用户推荐模块的 MyBatis 查询调通了,响应时间压到 150ms 以内。产品经理看了 demo 说“不错”,测试小姐姐也没提 bug。走在回出租屋的路上,夜风微凉,但心里踏实。
试用期还有三周,MyBatis 只是第一步。听说下周要上 Kafka 做异步解耦……唉,程序员的命,就是不断踩坑、填坑、再踩新坑。
但至少,我不再怕那个 BindingException 了。

评论 0