MyBatis入坑记:一个前端仔的后端初体验

App技术
2026-02-18 23:08
阅读 1227

上周五晚上十点半,办公室只剩我和运维老张还在死磕一个慢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 &lt;= #{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真的没想象中难。核心就三点:

  1. SQL写对(这是基本功)
  2. 映射配准(字段别名、驼峰转换)
  3. 动态灵活<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

最热最新
暂无评论
App技术Lv.1
0
影响力
0
文章
0
粉丝