MyBatis入坑记:一个前端仔的后端初体验
上周五晚上十点半,办公室只剩我和运维老张还在死磕一个慢SQL。他一边敲着EXPLAIN一边叹气:“你这查询,连索引都不走,数据库都要哭了。”我尴尬地摸了摸后脑勺——毕竟我可是那个写了三年Vue、连MySQL主从都没配过的纯种前端。
但事情总得有人干。两个月前刚入职这家二线互联网公司,原本以为能继续我的React+TypeScript舒适圈,结果领导一句“全栈是趋势”,直接把我塞进了新启动的内部运营平台项目组。更惨的是,后端同事刚好离职,留下的代码里一堆MyBatis XML文件,注释比代码还少,SQL写得跟散文诗似的。
没办法,只能硬着头皮学。好在我最近在研究Rust(纯粹是因为觉得unsafe指针很酷),对底层逻辑有点敏感,再加上Mac上装个Java环境也不难,干脆从零开始啃MyBatis。今天这篇技术分享,就是我这两周踩坑+填坑的真实记录,希望能帮到和我一样被迫“转岗”的前端兄弟们。
为什么选MyBatis?而不是JPA?
说实话,一开始我想直接上Spring Data JPA,毕竟它看起来更“现代化”——方法名就能生成SQL,像findByUserNameAndStatus()这种,简直是对前端友好的抽象。但团队里的老后端直接给我泼冷水:“JPA适合CRUD为主的系统,我们这个平台要跑复杂报表、多表关联、动态条件过滤,JPA搞不定。”
他还甩给我一张对比表:
| 特性 | MyBatis | Spring Data JPA |
|---|---|---|
| SQL控制粒度 | 精确到每一行 | 抽象层高,难以优化 |
| 动态SQL支持 | 原生支持(<if>, <foreach>) |
需自定义Query,复杂 |
| 性能调优空间 | 大(可手写高效SQL) | 小(依赖Hibernate生成) |
| 学习曲线 | 中等(需懂SQL) | 低(面向对象思维) |
| 适合场景 | 复杂查询、高性能要求 | 快速开发、简单业务 |
看到“性能调优空间大”这几个字,我心动了。毕竟前端出身的我对“性能”两个字特别敏感——就像看到未压缩的图片资源会浑身难受一样。
搭建第一个MyBatis项目:别被配置劝退
我用的是Spring Boot + MyBatis的经典组合。创建项目时选了Java 17(公司新项目统一标准),Maven管理依赖。关键依赖就这几个:
<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/ops_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.model
configuration:
map-underscore-to-camel-case: true # 数据库下划线自动转Java驼峰
坑点来了:一开始我没加map-underscore-to-camel-case: true,结果从数据库查出来的user_name字段在Java对象里是null,因为我的实体类属性叫userName。调试了半天才发现是命名策略问题。这种细节真的容易让人凌晨三点想砸MacBook(还好我只有一台Mac,Windows只用来测IE兼容性,不舍得砸)。
写第一个Mapper:XML vs 注解
MyBatis支持两种写法:XML映射文件 or 注解。我试了下注解:
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Long id);
看起来简洁,但一旦SQL复杂起来,比如带多个条件、分页、排序,注解里写SQL字符串简直灾难——没有语法高亮、不能换行、拼接困难。所以我果断转向XML。
建一个UserMapper.java接口:
public interface UserMapper {
User findById(Long id);
List<User> findActiveUsers();
void insert(User user);
}
对应的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="findById" resultType="User" parameterType="Long">
SELECT id, user_name as userName, status, created_at as createdAt
FROM user
WHERE id = #{id}
</select>
<select id="findActiveUsers" resultType="User">
SELECT id, user_name as userName, status, created_at as createdAt
FROM user
WHERE status = 'ACTIVE'
ORDER BY created_at DESC
</select>
<insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
INSERT INTO user (user_name, status, created_at)
VALUES (#{userName}, #{status}, NOW())
</insert>
</mapper>
注意几个细节:
resultType="User"能用是因为我在配置里设了type-aliases-package- 字段别名
as userName是为了让MyBatis自动映射到Java属性 useGeneratedKeys="true"是为了让插入后自动回填自增ID到对象的id字段
动态SQL:这才是MyBatis的杀手锏
我们平台有个用户筛选功能,产品经理要求支持按状态、创建时间范围、用户名模糊搜索,而且这些条件都是可选的。用传统JDBC拼SQL的话,得写一堆if判断字符串拼接,容易出错还可能SQL注入。
MyBatis的<where>和<if>简直是救星:
<select id="searchUsers" resultType="User">
SELECT id, user_name as userName, status, created_at as createdAt
FROM user
<where>
<if test="status != null and status != ''">
AND status = #{status}
</if>
<if test="startTime != null">
AND created_at >= #{startTime}
</if>
<if test="endTime != null">
AND created_at <= #{endTime}
</if>
<if test="keyword != null and keyword != ''">
AND user_name LIKE CONCAT('%', #{keyword}, '%')
</if>
</where>
ORDER BY created_at DESC
</select>
<where>标签会自动处理第一个AND的问题,不用手动trim。而且MyBatis默认会对参数做预编译,防SQL注入。当时写完这段,我激动地给后端群发了个“666”,结果被吐槽“前端味太重”。
性能优化:别让MyBatis变成慢查询制造机
上线前压测,发现一个列表接口TP99高达1.2秒。查日志发现是N+1问题:查100个用户,每个用户又去查他的角色信息,总共执行了101条SQL。
解决方案:用<resultMap>做关联查询。
先定义ResultMap:
<resultMap id="UserWithRoleMap" type="User">
<id property="id" column="id"/>
<result property="userName" column="user_name"/>
<association property="role" javaType="Role">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
</association>
</resultMap>
然后写一条JOIN查询:
<select id="findUsersWithRoles" resultMap="UserWithRoleMap">
SELECT u.id, u.user_name, r.id as role_id, r.name as role_name
FROM user u
LEFT JOIN user_role ur ON u.id = ur.user_id
LEFT JOIN role r ON ur.role_id = r.id
</select>
优化后TP99降到200ms以内。运维老张终于对我露出了微笑。
另外,记得开启二级缓存(谨慎使用):
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
但要注意:缓存适合读多写少、数据变化不频繁的场景。我们平台的操作日志表就不适合缓存,而字典表就很合适。
AI提效:Copilot帮我写XML?
说到AI提效,我最近试了GitHub Copilot写MyBatis XML。效果……一半一半。
当我输入注释:
<!-- 查询最近7天活跃用户,按登录次数降序 -->
Copilot居然真生成了合理的SQL:
<select id="findRecentActiveUsers" resultType="User">
SELECT u.*, COUNT(l.id) as loginCount
FROM user u
JOIN login_log l ON u.id = l.user_id
WHERE l.login_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY u.id
ORDER BY loginCount DESC
</select>
虽然resultType="User"这里有问题(多了loginCount字段),但框架是对的。修改起来比重头写快多了。不过复杂逻辑还是得自己把控,AI目前更像是“高级代码补全”,不是银弹。
给前端同学的建议:别怕碰后端
写完这个项目,我最大的感触是:前后端的界限其实没那么清晰。理解数据怎么来、怎么存、怎么查,能让我在写前端接口调用时更有底气。比如现在我知道分页为什么用LIMIT offset, size而不是前端全量拉取,也知道为什么有些字段要脱敏。
而且MyBatis真的没想象中难。核心就三点:
- SQL写对(这是基本功)
- 映射配准(字段别名、驼峰转换)
- 动态灵活(
<if>,<foreach>玩转条件)
如果你也像我一样被“赶鸭子上架”,别慌。从一个简单的CRUD开始,跑通流程,再逐步加复杂度。遇到报错别死磕,MyBatis的错误信息其实挺友好,比如:
Invalid bound statement (not found): com.example.mapper.UserMapper.findById
八成是XML的namespace写错了,或者没扫描到mapper文件。
最后:技术人的成长,往往始于“被迫”
回想这两个月,从看到Java报错就懵,到现在能独立设计数据库表结构、写高效SQL、配合运维调优,虽然过程痛苦(尤其是被产品经理临时改需求的时候),但收获巨大。
顺便说一句,我发现Rust的ownership思想其实在数据库连接池管理上也有影子——资源必须明确归属、及时释放。看来语言之间真的能互相启发。
所以,别抗拒跨领域学习。前端也好,后端也罢,解决问题的能力才是核心算法。至于工具,MyBatis也好,JPA也罢,不过是手中的剑。剑再锋利,也得看谁在用。
好了,我去改下一个需求了。听说这次要接入AI推荐算法……希望别让我写Python。(开玩笑的,其实已经在装Anaconda了。)

评论 0