MyBatis基础教程:一个文科生的Java持久层初体验
上个月底,我坐在成都春熙路附近一家咖啡馆里,耳机里放着陈绮贞的《旅行的意义》,手指在键盘上敲着Rust的match表达式——是的,作为一个非科班出身、大学读的是汉语言文学的前端仔,最近居然迷上了Rust。但生活总是充满反转:就在我沉浸于所有权和生命周期的哲学思考时,领导突然把我叫到会议室,轻描淡写地说:“小张啊,后端人手不够,你先帮忙搞下这个新项目的数据层,用MyBatis。”
我当时差点把冰美式打翻。我?一个连JDBC都只在面试题里见过的前端?还要碰Java持久层框架?
但没办法,项目deadline就在双11前,团队里后端大佬们全在救火另一个核心系统,而我这个“全栈潜力股”(其实是人手不够的替罪羊)只能硬着头皮上。于是,就有了这篇带着咖啡渍、深夜加班和无数Stack Overflow页面的MyBatis入门总结。
为什么是MyBatis?而不是直接写SQL或者用Hibernate?
说实话,一开始我以为持久层就是拼SQL字符串。直到我看到同事提交的代码里有一堆#{userId}和<if>标签,我才意识到:哦,原来有框架帮我们处理这些脏活。
我们项目是个电商后台管理系统,数据模型不算复杂,但查询条件多变——比如商品列表要支持按类目、价格区间、上架状态、关键词模糊搜索等组合筛选。如果手写JDBC,光是拼WHERE子句就能让我秃头。而MyBatis的动态SQL正好能优雅地解决这个问题。
相比之下,Hibernate虽然更“全自动”,但对SQL控制力弱,调试困难,而且团队里没人熟。而MyBatis“半自动”的特性——SQL你写,结果映射我帮你——对我们这种小团队来说刚刚好:既省去了大量样板代码,又能精准控制每一条SQL的性能。
Go?不,这次是Java
有朋友可能会问:“既然你在研究Rust,为什么不试试Go+GORM?”
哎,现实哪有那么理想。公司技术栈是Spring Boot + MySQL,历史包袱重,不可能为了一个内部工具项目推倒重来。再说了,产品经理上周五还说:“这个功能下周三必须上线,不然双11运营没法用。”——在这种DDL压迫下,能用现有技术栈快速交付才是王道。
从零搭建:MyBatis到底要配哪些东西?
我第一次跑通MyBatis,花了整整两天。不是因为难,而是因为文档太碎,各种配置项分散在不同地方,加上IDEA有时候抽风,XML文件死活不识别。
下面是我踩坑后整理的最小可行配置清单(基于Spring Boot 2.7 + MyBatis 3.5):
1. Maven依赖别漏了
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
注意:别手贱去单独引入mybatis包,starter已经包含了所有依赖。我一开始重复引入,导致版本冲突,启动时报了一堆ClassNotFoundException,当时真的想砸电脑。
2. application.yml 配置数据库
spring:
datasource:
url: jdbc:mysql://localhost:3306/ecommerce?useUnicode=true&characterEncoding=utf8&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驼峰
重点说下 map-underscore-to-camel-case。我们数据库字段命名习惯是create_time、user_id,而Java实体类是createTime、userId。开启这个选项后,MyBatis会自动映射,不用每个字段都写@Results注解——这对懒人(比如我)简直是福音。
3. 实体类 & Mapper接口
// User.java
public class User {
private Long id;
private String username;
private LocalDateTime createTime;
// getter/setter 省略
}
// UserMapper.java
@Mapper
public interface UserMapper {
User selectById(Long id);
List<User> selectByCondition(@Param("username") String username, @Param("status") Integer status);
int insert(User user);
}
注意:接口上要加@Mapper注解,或者在启动类上加@MapperScan("com.example.demo.mapper")。我一开始两个都没加,调用时报Invalid bound statement (not found),查了半天才发现是扫描问题。
动态SQL:MyBatis最香的地方
回到开头说的商品查询需求。假设我们要根据可选参数动态构建查询:
<!-- ProductMapper.xml -->
<select id="selectProducts" resultType="Product">
SELECT id, name, price, category_id, status, create_time
FROM product
WHERE 1=1
<if test="categoryId != null">
AND category_id = #{categoryId}
</if>
<if test="minPrice != null">
AND price >= #{minPrice}
</if>
<if test="maxPrice != null">
AND price <= #{maxPrice}
</if>
<if test="keyword != null and keyword != ''">
AND name LIKE CONCAT('%', #{keyword}, '%')
</if>
ORDER BY create_time DESC
</select>
这段XML看起来平平无奇,但比手写JDBC拼字符串安全多了——MyBatis会自动处理参数注入,避免SQL注入风险。而且<if>标签让逻辑清晰,测试也方便。
不过要注意:< 要写成 <,否则XML解析会报错。我第一次写price <= #{maxPrice},启动直接失败,还以为是MyBatis bug,结果是XML语法问题……文科生的痛谁懂。
性能与生产环境:别只顾着跑通
本地跑通只是开始,上线才是考验。我们在预发环境压测时发现,某个列表接口QPS只有50,而DB CPU飙到90%。排查后发现是没加索引 + 全表扫描。
优化经验总结:
| 问题 | 解决方案 | 效果 |
|---|---|---|
| 没有WHERE条件导致全表扫描 | 在高频查询字段(如status, category_id)加复合索引 | QPS提升至300+ |
| N+1查询问题(查用户+订单) | 使用<collection>或分两次查+内存合并 |
RT降低60% |
| 大结果集内存溢出 | 分页查询 + 设置fetchSize | 内存稳定 |
特别说下N+1问题。比如要查10个用户的订单列表,如果用嵌套查询:
<resultMap id="UserWithOrders" type="User">
<id property="id" column="id"/>
<collection property="orders" ofType="Order"
select="selectOrdersByUserId" column="id"/>
</resultMap>
这会导致1次查用户 + N次查订单(N=10),共11条SQL。在高并发下数据库连接池很容易被打满。后来我们改成先查用户ID列表,再批量查订单,虽然代码多两行,但性能稳如老狗。
和前端联调那些事儿
作为前端出身,我特别在意接口设计。所以在写Mapper时,我会主动和前端同学对齐:
- 分页统一用
pageNo/pageSize,返回{list, total} - 时间字段统一为ISO8601格式(
2023-10-01T12:00:00) - 错误码规范,别返回“系统异常”这种鬼话
有一次,测试同学提了个bug:“商品状态显示‘1’而不是‘上架’”。我一查,发现后端直接把数据库的int状态字段透传给前端了。赶紧加了个@JsonFormat注解做枚举转换:
public enum ProductStatus {
OFF_SHELF(0, "下架"),
ON_SHELF(1, "上架");
private final int code;
private final String desc;
// 构造函数、getter...
}
并在ResultMap里指定typeHandler。从此前端再也不用自己维护状态映射表了——这种前后端协作的小细节,往往比炫技更重要。
踩过的坑 & 血泪教训
XML文件不生效
原因:Maven默认不打包src/main/java下的XML文件。解决:在pom.xml加resource配置:<build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>空字符串 vs null
MyBatis默认把空字符串""当作有效值,不会触发<if test="xxx != null">。如果业务上空字符串应视为未输入,需额外判断:<if test="keyword != null and keyword != ''">事务失效
在Service方法上忘记加@Transactional,导致插入主表成功但关联表失败时无法回滚。后来我们团队约定:所有写操作必须走Service层,且加事务注解。
代码人生:从抗拒到真香
写这篇文章的时候,窗外成都的雨淅淅沥沥。回想两个月前那个面对MyBatis一脸懵的自己,现在居然能独立完成整个数据层模块,甚至开始给后端同事Review SQL了。
虽然我还是更爱写CSS动画和React Hooks,但这次被迫“跨界”让我明白:技术栈的边界,很多时候是自己画的牢笼。前端也好,后端也罢,底层逻辑都是相通的——抽象、复用、性能、可维护性。
而且,当你能看懂后端代码,和后端沟通时就不再是“能不能加个字段”这种低级提问,而是能讨论“这个查询能不能走覆盖索引”、“分页要不要用游标”。这种跨职能的理解力,在小团队里尤其珍贵。
至于Rust?它还在我的学习清单里。但至少现在,我能一边听着City Pop,一边淡定地写<foreach collection="ids" item="id">了。
最后一点建议
如果你和我一样,是非科班出身,被临时抓壮丁去写后端:
- 别怕,MyBatis的学习曲线其实很平缓
- 先跑通一个CRUD,再逐步加复杂功能
- 多看生成的SQL日志(开
logging.level.com.yourpackage.mapper=debug) - 生产环境一定要压测 + 监控慢查询
代码人生,从来不是非黑即白。前端可以懂SQL,文科生也能玩转ORM。重要的不是你从哪里来,而是你愿意往哪里去。
哦对了,双11那天,我们的系统扛住了流量高峰,零故障。运维大哥请我喝了杯奶茶,说:“前端小子,干得不错。”——那一刻,所有的深夜加班都值了。
(完)

评论 0