MyBatis上手踩坑实录:从外包狗到跳槽党的自救指南

韩思宇★
2025-12-28 17:13
阅读 527

上周五晚上十点半,我盯着屏幕上那行红得发紫的报错:“Invalid bound statement (not found): com.example.mapper.UserMapper.selectUserById”,心里一万只羊驼奔腾而过。这已经是本周第三次因为 MyBatis 配置问题加班了——产品经理明天就要演示新功能,而我连数据库都连不上。

说来惭愧,干了四年外包,写过 Java、改过 Python 脚本、甚至还帮前端同事 debug 过 Go 服务(别问,问就是“全栈”),但 MyBatis 这玩意儿一直靠复制粘贴糊弄过去。直到最近准备跳槽,刷面经发现“讲讲 MyBatis 一级缓存”、“MyBatis 和 Hibernate 区别”这类问题高频出现,我才意识到:再不系统学一遍,简历都投不出去。

于是,我决定用最笨但也最有效的方式——从零搭一个能跑的项目,把每个坑都踩一遍。这篇文章就是我的血泪笔记,希望能帮同样在“外包深水区”挣扎的兄弟少走点弯路。


为啥不用 JPA?非得啃 MyBatis?

先说背景。我们公司接了个政府项目,数据库是 Oracle(对,就是那个老古董),表结构复杂得像迷宫,字段命名全是 USR_NMECRT_DTTM 这种反人类风格。客户还要求所有 SQL 必须可审计、可调优,不能让 ORM 框架“偷偷”生成一堆看不懂的语句。

这时候 JPA 就有点力不从心了。虽然它在标准 CRUD 上很优雅,但一旦涉及复杂关联查询、动态条件拼接,HQL 写起来比原生 SQL 还绕。而 MyBatis 的核心思想很简单:你写 SQL,我帮你映射。SQL 你完全掌控,想怎么优化就怎么优化,特别适合我们这种“甲方爸爸说了算”的外包场景。

顺便吐槽一句:隔壁组用 Python 写数据清洗脚本,动不动就 pandas.merge() 一把梭,结果线上跑崩三次;还有个 Go 微服务团队,用 GORM 玩嵌套预加载,N+1 问题差点把数据库干趴下。相比之下,MyBatis 虽然配置麻烦点,但至少“所见即所得”。


三步走:从 Hello World 到能跑通

第一步:别被 Maven 依赖劝退

很多人卡在第一步:依赖版本不对,或者漏了某个包。我直接贴出能跑的 pom.xml 片段(MySQL 示例,Oracle 类似):

<dependencies>
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.13</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>8.0.33</version>
    </dependency>
    <!-- 别忘了这个!否则会报 ClassNotFound -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>2.0.7</version>
    </dependency>
</dependencies>

注意:不要用最新版 MyBatis!去年双11期间我们升级到 3.5.14,结果批量插入性能下降 40%,回滚后才恢复。稳定压倒一切,尤其在外包项目里——毕竟谁也不想半夜被运维电话叫醒。

第二步:配置文件别瞎配

MyBatis 的核心是 mybatis-config.xml。新手常犯的错误是把 mapper 路径写错,或者没注册别名。看我精简后的配置:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/test?useSSL=false"/>
        <property name="username" value="root"/>
        <property name="password" value="123456"/>
      </dataSource>
    </environment>
  </environments>
  
  <!-- 关键!指定 mapper XML 文件位置 -->
  <mappers>
    <mapper resource="mappers/UserMapper.xml"/>
  </mappers>
</configuration>

这里有个坑:resource 路径是相对于 src/main/resources 的。如果你把 XML 放在 src/main/java 下,IDE 可能不会自动复制到 classpath,导致运行时报 “not found”。解决办法要么移动文件,要么在 Maven 里加 resource 处理规则——但作为外包狗,我建议直接放 resources 目录,省事。

第三步:写个能跑的 Mapper

实体类 User.java

public class User {
    private Long id;
    private String name;
    private Integer age;
    // getter/setter 略
}

Mapper 接口:

public interface UserMapper {
    User selectUserById(Long id);
    void insertUser(User user);
}

对应的 UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
  PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.UserMapper">
  <select id="selectUserById" resultType="com.example.model.User">
    SELECT id, name, age FROM users WHERE id = #{id}
  </select>
  
  <insert id="insertUser" parameterType="com.example.model.User">
    INSERT INTO users (name, age) VALUES (#{name}, #{age})
  </insert>
</mapper>

注意 namespace 必须和接口全限定名一致!我第一次写成 UserMapper(少了包名),调试半小时才发现。


动态 SQL:MyBatis 的真正杀招

外包项目最怕什么?需求变!今天要按姓名查,明天要按年龄范围,后天又要组合查询。这时候 MyBatis 的 <if><choose><foreach> 就派上用场了。

比如这个万能查询:

<select id="searchUsers" resultType="User">
  SELECT * FROM users
  <where>
    <if test="name != null and name != ''">
      AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="minAge != null">
      AND age >= #{minAge}
    </if>
    <if test="maxAge != null">
      AND age &lt;= #{maxAge}
    </if>
  </where>
</select>

<where> 标签会自动去掉第一个 AND,避免语法错误。这比在 Java 里拼字符串安全多了——还记得去年那个 SQL 注入漏洞吗?测试部差点把我开了。

再比如批量插入,用 <foreach>

<insert id="batchInsert">
  INSERT INTO users (name, age) VALUES
  <foreach collection="list" item="user" separator=",">
    (#{user.name}, #{user.age})
  </foreach>
</insert>

注意:MySQL 默认不支持多值插入,需要加 rewriteBatchedStatements=true 到 JDBC URL 里,否则性能还不如单条循环。这种细节,文档里不写,全靠踩坑积累。


性能与缓存:别被“自动”迷惑

MyBatis 有一级缓存(SqlSession 级别)和二级缓存(Mapper 级别)。听起来很美好,但实际使用要小心。

一级缓存默认开启,在同一个 SqlSession 中,重复查询相同 ID 会命中缓存。但一旦执行了增删改,缓存就清空。这在 Web 应用中基本无效,因为每次请求都是新 SqlSession。

二级缓存需要手动开启,且要求实体类实现 Serializable。更坑的是,跨 SqlSession 的缓存可能导致脏读!比如 Session A 更新了用户,但 Session B 仍读到旧数据。我们线上就因此出现过订单状态不一致的问题。

所以我的建议是:除非你完全理解缓存机制,否则关掉二级缓存。用 Redis 做应用层缓存更可控。毕竟在外包项目里,稳定性比那点性能提升重要得多。


MyBatis vs 其他语言生态

有人问:现在都 2024 年了,为啥还用 MyBatis?看看 Go 和 Python 的 ORM:

  • Go 的 GORM:链式调用很爽,但复杂查询还是得写原生 SQL,而且 N+1 问题防不胜防。
  • Python 的 SQLAlchemy:功能强大,但学习曲线陡峭,小项目用它有点杀鸡用牛刀。

而 MyBatis 的优势在于:Java 生态成熟 + SQL 完全可控 + 社区资源丰富。对于需要长期维护、多人协作的外包项目,这种“显式优于隐式”的设计反而更安全。


最后:外包狗的自救心得

学 MyBatis 的过程让我明白:技术深度比广度更重要。以前我总觉得自己会七八种语言很牛,但面试官一问底层原理就露馅。现在每天刷两道 LeetCode,啃一遍《MyBatis 技术内幕》,感觉底气足多了。

如果你也在外包公司混日子,别等甲方提需求才学新技术。趁现在,把基础打牢——说不定下个月跳槽,你就成了别人眼中的“高级工程师”。

对了,文中的代码我都放 GitHub 了(链接略),欢迎 star。要是你也在被 MyBatis 折磨,评论区一起吐槽!

评论 0

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