MyBatis基础教程:Java持久层框架入门
上周五晚上十点半,实验室只剩我和隔壁工位的阿哲还在“肝”项目。成都的初夏夜风从窗户缝里钻进来,配上空调外机嗡嗡的噪音,还真有点程序员深夜加班的氛围感。导师布置的课题系统要对接新数据库,而我之前只会用JDBC裸写SQL——那玩意儿在研一课程设计里还能糊弄过去,放到真实项目里简直就是自虐。
被逼无奈之下,我翻开了MyBatis的官方文档。说来惭愧,作为211软件工程研二狗,在实验室搞了快一年后端开发,居然还没正经用过主流ORM框架。好在Mac上IntelliJ IDEA跑得飞起,配好环境后,总算没让Windows测试机提前“退休”。
为啥非得用MyBatis?
先说背景:我们这个项目是给本地一家社区医院做的预约挂号系统,前后端分离架构。前端同学(对,就是那个总在群里@我说接口又404的小王)用Vue3 + TypeScript,后端是我一个人扛着Spring Boot + MySQL。本来以为CRUD能随便应付,结果产品经理上周突然加需求:“医生排班要支持按科室、日期、时间段三级联动筛选”——好家伙,SQL直接复杂度爆炸。
如果继续手写JDBC,光是拼接动态WHERE条件就得让我秃头。而且团队Code Review时,师兄指着我那段if (name != null) sql += " AND name = ?"直摇头:“你这代码,等哪天要加个模糊查询,怕不是得重写整个DAO?”
于是,MyBatis成了救命稻草。
初体验:配置比想象中简单
MyBatis的核心思想其实很朴素:把SQL从Java代码里解放出来,但又不像Hibernate那样完全隐藏SQL。对于我们这种既要控制性能、又不想被SQL字符串折磨的开发者来说,简直是甜点。
我的pom.xml只加了两个依赖:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
然后在application.yml里配好数据源(成都这地方网络偶尔抽风,所以连接池参数我特意调大了点):
spring:
datasource:
url: jdbc:mysql://localhost:3306/hospital?useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
hikari:
maximum-pool-size: 20
connection-timeout: 30000
关键一步来了:Mapper接口 + XML映射文件。比如我要查医生信息:
// DoctorMapper.java
public interface DoctorMapper {
List<Doctor> selectByConditions(@Param("deptId") Integer deptId,
@Param("date") LocalDate date);
}
对应的DoctorMapper.xml:
<select id="selectByConditions" resultType="com.lab.hospital.entity.Doctor">
SELECT * FROM doctor
WHERE status = 1
<if test="deptId != null">
AND department_id = #{deptId}
</if>
<if test="date != null">
AND EXISTS (
SELECT 1 FROM schedule
WHERE doctor_id = doctor.id
AND schedule_date = #{date}
)
</if>
</select>
看到没?动态SQL直接写在XML里,逻辑清晰到连前端小王都能看懂(他后来真来看了,还吐槽说“你们后端终于不用if拼SQL了”)。
踩坑实录:那些让我想砸MacBook的瞬间
当然,路不可能一帆风顺。第一个大坑就是驼峰命名转换。数据库字段叫create_time,Java实体类是createTime,结果查出来全是null。查了半天文档才发现要加配置:
mybatis:
configuration:
map-underscore-to-camel-case: true
第二个坑更隐蔽:事务管理。我在Service层写了@Transactional,但方法内部调用另一个Service方法时,事务居然不生效!后来才明白这是Spring AOP的代理机制问题——同一个类内的方法调用不会触发代理。解决办法要么拆成两个Bean,要么用AopContext.currentProxy()(不推荐),最后我老老实实用了第一种。
还有一次线上事故:前端传了个超长的医生姓名(可能是测试时乱填的),结果MySQL报Data too long for column 'name'。虽然根本原因是前端没做长度校验,但作为后端,我也得兜底。于是在MyBatis里加了参数校验拦截器——不过这是进阶内容了,新手先保证功能跑通再说。
性能与可维护性:别只顾着跑通就交差
我们实验室有个不成文的规定:代码提交前必须过SonarQube扫描。有一次我写了个N+1查询(在一个循环里反复查排班表),被直接打回。MyBatis其实提供了<collection>标签做关联查询,避免多次访问数据库:
<resultMap id="DoctorWithSchedules" type="Doctor">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="schedules" ofType="Schedule">
<id property="id" column="schedule_id"/>
<result property="date" column="schedule_date"/>
</collection>
</resultMap>
配合一条JOIN SQL,查询次数从N+1降到1次。在高并发场景下,这种优化能救命——去年双11我们学校电商实训项目就因为N+1查询差点把数据库干崩。
另外,SQL可读性真的很重要。别为了炫技写超复杂的嵌套子查询。有一次我看到实习生写的SQL里有三层EXISTS,当场血压飙升。现在我们团队约定:超过5行的SQL必须写注释,复杂逻辑优先考虑用视图或存储过程(虽然我不太爱用存储过程,但有时候真香)。
给前端同学的“友好提示”
说到前端,其实MyBatis间接影响了他们的开发体验。比如我们统一了分页接口格式:
{
"code": 200,
"data": {
"list": [...],
"total": 100,
"pageNum": 1,
"pageSize": 10
}
}
后端用MyBatis-Plus(MyBatis的增强工具)的PageHelper插件一行代码搞定分页,前端小王再也不用自己算offset了。他还偷偷跟我说:“你们后端要是早点用这玩意儿,我上周就不会因为分页参数错乱熬到凌晨三点了。”
开发心得:工具只是手段,思维才是核心
折腾了两周,系统终于稳定上线。复盘下来,MyBatis最大的价值不是“自动映射”,而是强制你把SQL当作一等公民来对待。它不像某些全自动ORM那样让你忘记数据库的存在,也不像JDBC那样让你淹没在字符串拼接里。
作为研究生,我越来越觉得:好的代码不仅要跑得对,还要让人看得懂、改得动、测得全。MyBatis在这点上做得很好——SQL写在XML里,天然具备可读性;Mapper接口清晰定义了数据契约;配合良好的日志配置(记得开mybatis.configuration.log-impl=STDOUT_LOGGING),调试起来也方便。
最后吐个槽:运维大哥昨天又抱怨日志太大,说MyBatis打印的SQL占了80%空间。我默默把日志级别从DEBUG改成INFO……成年人的世界,果然都是妥协。
如果你也在成都搞Java开发,欢迎来茶店子附近约杯咖啡聊聊技术(或者一起吐槽产品经理)。毕竟在这个节奏舒服的城市,代码可以慢慢写,但架构一定要稳。

评论 0