MyBatis 基础教程:一个前端仔被迫啃 Java 持久层的血泪史
大家好,我是小陈,一个在组里写了快两年 React/Vue 的纯前端。每天早上 8 点准时出现在工位(别问,问就是早起型选手,而且我司后端大佬们习惯 7:30 开站会,我怕迟到被拖去改 SQL)。
说实话,写这篇 MyBatis 入门教程的时候,我内心是有点虚的——毕竟我主战场在浏览器里,Node.js 刚刚摸到 Express 和 Koa 的边儿,突然被拉去搞 Java 后端项目,属实有点“前端人被迫营业”的既视感。
事情要从去年双11前说起。我们组接了个新需求:给老后台系统加个数据看板模块,但前端只负责展示,数据接口得自己对接数据库。原计划是让 Java 后端同事帮忙写接口,结果人家一句:“你不是学 Node.js 想做全栈吗?正好练手,用 MyBatis 写个 DAO 层吧。” 我当场瞳孔地震。
更扎心的是,面试题库里最近全是 “MyBatis 和 Hibernate 有啥区别?”、“#{} 和 ${} 有什么安全风险?”——很明显,这玩意儿已经成了 Java 后端岗的标配。作为一个想跳槽涨薪的前端,不啃它,简历都过不了筛。
于是,在上周五晚上 9 点、咖啡续命第 3 杯、被产品经理催着“明天必须联调”的 deadline 压力下,我硬着头皮把 MyBatis 给撸通了。今天就把我踩过的坑、学到的干货,用前端视角+实战经验,给大家盘一盘。
为啥是 MyBatis?而不是直接写 JDBC 或者用 Python?
先说背景:我们公司技术栈偏保守,后端基本是 Spring Boot + MySQL + MyBatis 三件套。运维那边连 Redis 版本升级都要开三次评审会,更别说让我用 Node.js 直连生产数据库了——运维大哥直接回我:“你怕不是想炸库?”
那为什么不用 Python 写个 Flask 接口?因为……测试环境数据库权限只对 Java 应用开放(别问,问就是“历史遗留架构”)。所以,没得选,只能上 MyBatis。
但 MyBatis 其实挺香的。对比原始 JDBC,它帮你自动映射 ResultSet 到 Java Bean;对比 Hibernate 这种全自动 ORM,它又保留了写 SQL 的自由度——这点对我这种习惯写复杂查询的前端(没错,我在 Node 里也手写 SQL)特别友好。
前端吐槽时间:
在 JavaScript 里,我们习惯user.name直接取值;但在 Java 里,你得写user.getName()。第一次看到 MyBatis 自动把数据库字段user_name映射成 Java 对象的userName,我真的感动哭了——这不比手动row[‘user_name’]强?
实战:从零搭一个 MyBatis 查询接口
第一步:建表 & 定义实体类
假设我们要查用户信息,数据库表如下:
CREATE TABLE `t_user` (
`id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`user_name` VARCHAR(50) NOT NULL,
`email` VARCHAR(100),
`created_at` DATETIME
);
对应的 Java 实体类(User.java):
public class User {
private Long id;
private String userName; // 注意:这里用驼峰,对应数据库下划线 user_name
private String email;
private LocalDateTime createdAt;
// getter / setter 省略(IDEA 一键生成)
}
坑点提醒:
数据库字段created_at要映射到createdAt,需要开启 MyBatis 的 驼峰命名自动转换,否则查出来全是 null!配置如下(后面会讲)。
第二步:写 Mapper 接口和 XML
MyBatis 的核心思想是“接口 + XML 配置”。你只需要定义一个接口,MyBatis 会在运行时动态生成实现类。
UserMapper.java:
public interface UserMapper {
User selectById(Long id);
List<User> selectAll();
}
UserMapper.xml(放在 resources/mapper/ 下):
<?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.mapper.UserMapper">
<select id="selectById" resultType="com.example.entity.User">
SELECT id, user_name, email, created_at
FROM t_user
WHERE id = #{id}
</select>
<select id="selectAll" resultType="com.example.entity.User">
SELECT id, user_name, email, created_at
FROM t_user
</select>
</mapper>
注意这里的 #{id} —— 这是 预编译占位符,能有效防止 SQL 注入。如果你写成 ${id},那就等于直接拼字符串,黑客传个 1 OR 1=1 就能把你库拖走。这题可是高频面试题!
第三步:配置 MyBatis(Spring Boot 版)
在 application.yml 里配数据源和 MyBatis 设置:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true # 关键!开启下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL 日志
运维经验:
生产环境一定要关掉log-impl,否则每条 SQL 都打日志,IO 直接爆掉。我们去年就因为这个,凌晨三点被 PagerDuty 叫醒——日志磁盘满了,服务挂了。
第四步:在 Service 里调用
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User getUser(Long id) {
return userMapper.selectById(id);
}
}
Controller 返回 JSON 给前端,完美对接!
面试题高频考点 & 我踩过的坑
1. #{} vs ${} —— 安全性的生死线
#{}:预编译,参数会被当作 占位符 处理,安全。${}:直接字符串替换,有 SQL 注入风险,仅用于动态表名、列名等场景(比如分表)。
真实事故:
上周实习生写了个${orderBy}做排序,结果被测试同学输入id; DROP TABLE t_user--,本地测试库直接没了。还好是测试环境……领导当场让他背了 10 遍《Java 安全开发规范》。
2. 一级缓存 & 二级缓存 —— 别乱开!
MyBatis 默认开启 一级缓存(SqlSession 级别),同一个会话里重复查询会走缓存。但在 Web 应用中,每次请求都是新 SqlSession,所以基本无效。
二级缓存(Mapper 级别)可以跨会话,但要注意:
- 缓存的是 序列化后的对象,实体类必须 implements Serializable
- 多节点部署时,缓存不同步,容易脏读
建议:除非 QPS 极高且数据变更少(比如字典表),否则别开二级缓存。我们组现在统一用 Redis 做缓存,MyBatis 只负责查库。
3. 动态 SQL —— <if>、<foreach> 是神器
比如根据条件模糊搜索:
<select id="searchUsers" resultType="User">
SELECT * FROM t_user
<where>
<if test="userName != null and userName != ''">
AND user_name LIKE CONCAT('%', #{userName}, '%')
</if>
<if test="email != null">
AND email = #{email}
</if>
</where>
</select>
再比如批量插入(前端传数组,后端用 foreach):
<insert id="batchInsert">
INSERT INTO t_user (user_name, email) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.userName}, #{user.email})
</foreach>
</insert>
这比在 JavaScript 里拼字符串优雅多了好吗!
性能与生产建议
| 优化项 | 建议 | 理由 |
|---|---|---|
| SQL 日志 | 开发开,生产关 | 避免 IO 瓶颈 |
| 分页查询 | 用 PageHelper 插件 |
避免 SELECT * 全表扫描 |
| 连接池 | 用 HikariCP(Spring Boot 默认) | 性能最好,配置简单 |
| 字段映射 | 开启 mapUnderscoreToCamelCase |
避免手写 resultMap |
架构思考:
虽然我是个前端,但我发现很多 Java 同事写 MyBatis 时,喜欢在 XML 里塞超复杂逻辑(嵌套子查询+多表 join+case when)。这其实违背了“单一职责”——DAO 层应该只做数据存取,复杂逻辑交给 Service 层或数据库视图。下次 code review 我打算拿这个怼回去(狗头保命)。
写在最后:前端学 MyBatis 值不值?
值!太值了!
- 拓宽技术视野:理解后端怎么查库,联调时不再只会喊“接口怎么又 500 了?”
- 跳槽加分项:很多大厂要求“了解主流后端框架”,MyBatis 是 Java 生态绕不开的一环
- 全栈底气:以后用 Node.js 写 API 时,也能借鉴 MyBatis 的设计思想(比如我最近在研究 TypeORM)
当然,我也不会放弃前端老本行。只是现在,当后端同事再说“你不懂数据库”,我可以微微一笑:“你 MyBatis 的二级缓存开了吗?”
今日份自嘲:
昨天提交代码,Git commit message 写成了 “fix: 修复 user list 接口,终于能跑通了,前端人写 Java 真难”。结果被组长看到了,回我:“下次用英文,显得专业点。” …… 好吧,我改成 “feat: implement user query with MyBatis, survive as a frontend dev”。
如果你也是前端,正在向全栈进发,别怕 Java,别怕 MyBatis。毕竟,能写出 SELECT * FROM t_life WHERE hope > 0 的人,还有什么坎过不去呢?
共勉。

评论 0