MyBatis 基础教程:一个前端仔被迫啃 Java 持久层的血泪史

写码的阿川
2025-12-17 10:17
阅读 209

大家好,我是小陈,一个在组里写了快两年 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

最热最新
暂无评论
匿名用户Lv.1
0
影响力
0
文章
0
粉丝