MyBatis 基础教程:Java 持久层框架入门 —— 一个传统企业后端开发的血泪史

青山不改需求改
2025-12-15 09:59
阅读 700

上周五晚上九点半,我正坐在北京回龙观地铁站外的711门口啃关东煮,手机突然弹出一条钉钉消息:“老大说下周三上线新功能,数据库交互模块用 MyBatis 重写。”我差点把丸子吐出来——不是因为难吃,是因为这需求来得太猝不及防。

我是谁?一个在北京某传统制造企业数字化转型部门苟延残喘的 Java 后端开发。每天通勤一小时,工资没涨,但 PPT 和周报越写越多。我们团队去年才从 Spring JDBC 转型到 MyBatis,说是为了“提升系统可维护性”(其实是领导看了某篇公众号文章后拍脑袋决定的)。而我,一个靠 ChatGPT 写 CRUD、靠 Claude 优化 SQL 的社畜,被迫在 deadline 前重新审视这个被无数 Java 程序员又爱又恨的持久层框架。

今天这篇文章,不搞那些花里胡哨的“十分钟上手”,也不复制粘贴官网文档。我就聊聊我自己怎么从“MyBatis 是啥?”到“哦,原来 XML 里还能这么玩”的心路历程。顺便,也给正在刷简历准备跳槽的兄弟们一点真实参考——毕竟现在连做区块链钱包后台的公司都要求你会 MyBatis 了(别问,问就是 Web3.0 + Java 后端组合拳)。


为什么不用 Hibernate?也不是 Python?

先别急着喷我“都2024年了还用 MyBatis”。我们公司是个典型的“传统行业+数字化外壳”混合体。业务逻辑复杂,历史数据堆积如山,DBA 对 SQL 控制欲极强——他们甚至不允许 ORM 自动生成 ALTER TABLE。在这种环境下,Hibernate 那种全自动映射根本活不下去。你写个 @OneToMany,DBA 直接给你打回来:“你这 N+1 查询能上生产?”

至于 Python?别笑。我们隔壁数据中台组确实用 Django + SQLAlchemy 跑得飞起,但他们处理的是离线分析任务,QPS 不超过 50。而我们的订单系统,双11期间峰值 QPS 3000+,每个 SQL 都要经过 DBA 审核、压测、慢查询监控。在这种场景下,可控性 > 开发效率。MyBatis 让我们能手写每一条 SQL,还能精细调优——这才是传统企业后端的真实生存法则。


MyBatis 到底解决了什么问题?

简单说:解耦 SQL 与 Java 代码

还记得早期用 JDBC 时那种地狱吗?

String sql = "SELECT user_id, name, email FROM users WHERE status = ? AND created_at > ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 1);
ps.setDate(2, new Date(...));
// ... 然后还要手动 setXXX, getXXX, 关闭资源

写多了真的想砸键盘。更别说 SQL 散落在各个 Service 里,改个字段要全局搜索。MyBatis 的核心思想就一个:把 SQL 抽出来,放到 XML 或注解里,Java 只负责传参和接收结果

比如一个简单的用户查询:

<!-- UserMapper.xml -->
<select id="selectUserById" resultType="com.example.User">
    SELECT user_id, name, email 
    FROM users 
    WHERE user_id = #{userId}
</select>

Java 代码变成:

User user = userMapper.selectUserById(123L);

是不是清爽多了?而且 DBA 想优化这条 SQL,直接改 XML 文件就行,不用动 Java 代码——这对运维来说简直是福音。


配置踩坑实录:别信官网 Quick Start

官方文档写得跟童话似的:“只需三步,轻松集成!”
现实是:我第一次跑起来花了整整两天,主要卡在 配置文件路径Mapper 扫描 上。

我们项目用的是 Spring Boot + MyBatis,按理说加个 mybatis-spring-boot-starter 就行。但问题来了:XML 文件放哪?

默认情况下,MyBatis 只扫描 resources 下的 mapper XML。但我们的项目结构是:

src/
├── main/
│   ├── java/
│   │   └── com/example/mapper/UserMapper.java
│   └── resources/
│       └── mapper/UserMapper.xml

结果启动时报错:

org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectUserById

查了半天,才发现是 Maven 默认不打包 XML 文件!得在 pom.xml 里加:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <!-- 关键:把 java 目录下的 xml 也打包 -->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
    </resources>
</build>

或者更优雅的做法:把 XML 放在 resources/mapper 下,并在 application.yml 里指定路径

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.model

这种坑,文档里提一句“注意资源过滤”,但新人根本不知道这意味着什么。我当时真想给 MyBatis 团队寄一箱螺蛳粉表示“感谢”。


动态 SQL:MyBatis 最香的功能

传统企业最怕什么?灵活查询条件。产品经理张口就来:“用户列表要支持按姓名、手机号、注册时间、状态、渠道等任意组合筛选。”

用 JDBC 写?拼字符串拼到怀疑人生。MyBatis 的 <if><choose><foreach> 直接封神。

比如一个带多条件的订单查询:

<select id="selectOrders" resultType="Order">
    SELECT order_id, user_id, amount, status, created_at
    FROM orders
    WHERE 1=1
    <if test="userId != null">
        AND user_id = #{userId}
    </if>
    <if test="statusList != null and statusList.size() > 0">
        AND status IN
        <foreach collection="statusList" item="status" open="(" separator="," close=")">
            #{status}
        </foreach>
    </if>
    <if test="startDate != null">
        AND created_at >= #{startDate}
    </if>
</select>

注意那个 WHERE 1=1——这是老派写法,其实 MyBatis 提供了 <where> 标签自动处理 AND/OR 前缀:

<where>
    <if test="userId != null">AND user_id = #{userId}</if>
    <if test="statusList != null">AND status IN ... </if>
</where>

这样更干净。但千万别在 <where> 里写 WHERE 关键字,否则生成的 SQL 会变成 WHERE WHERE ...,线上直接爆炸。


性能陷阱:别让 MyBatis 成为拖油瓶

MyBatis 本身很轻量,但用不好照样拖垮系统。我们去年双11就栽过一次。

问题出在 N+1 查询。场景是:查一批订单,每个订单要关联用户信息。

错误写法:

List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
    User user = userMapper.selectById(order.getUserId()); // 每次都查一次 DB!
}

1000 个订单 → 1001 次数据库查询。DBA 当场报警。

正确做法:一次查出所有关联数据

方案一:用 <collection> 做嵌套查询(慎用,可能更慢)
方案二(推荐):先查订单,再批量查用户 ID,最后内存关联。

List<Order> orders = orderMapper.selectAll();
List<Long> userIds = orders.stream().map(Order::getUserId).collect(Collectors.toList());
List<User> users = userMapper.selectByIds(userIds); // SELECT * FROM users WHERE user_id IN (...)

Map<Long, User> userMap = users.stream().collect(Collectors.toMap(User::getUserId, u -> u));
orders.forEach(order -> order.setUser(userMap.get(order.getUserId())));

虽然多了一次查询,但总次数从 N+1 降到 2,性能天壤之别。


和“高大上”技术的关系:区块链?简历?

你可能会问:这玩意儿和区块链有啥关系?

说实话,没啥直接关系。但如果你在写简历,尤其是想跳槽去搞 Web3 或金融科技的公司,MyBatis 几乎是标配。为什么?因为这些公司虽然前端玩 Solidity、Rust,但后端管理平台、用户系统、交易对账模块,依然用 Java + MySQL + MyBatis 这套稳如老狗的组合。

我上个月帮一个朋友改简历,他项目写“使用 Spring Data JPA”,我直接改成“MyBatis + 手写 SQL 优化,支撑日均百万级交易”。HR 筛简历时,看到“手写 SQL”、“性能优化”这种词,眼睛都亮了——毕竟 JPA 在复杂查询面前太弱鸡。

另外,很多所谓“区块链项目”的后端,本质还是传统业务系统。比如一个数字藏品平台,铸币记录存链上,但用户信息、订单、支付流水全在 MySQL 里。这时候 MyBatis 就成了连接链上链下的关键桥梁。


生产环境经验:监控与调优不能少

在传统企业,上线只是开始。我们运维有一套严格的 MyBatis 监控规范:

  1. 开启 SQL 日志(仅测试环境):

    logging:
      level:
        com.example.mapper: debug
    

    别在生产开!否则日志文件一天爆 50G。

  2. 慢查询监控:所有 SQL 必须走 APM(我们用 SkyWalking),超过 200ms 自动告警。

  3. 防止 SQL 注入:永远用 #{},别用 ${}。除非你明确知道自己在做什么(比如动态表名),否则别碰。

  4. 缓存策略:MyBatis 二级缓存默认关闭,我们也没敢开——分布式环境下容易脏读。宁可多查几次 DB,也不敢赌数据一致性。


MyBatis vs 其他方案对比

方案 优点 缺点 适用场景
MyBatis SQL 可控、性能高、学习曲线平缓 需手写 SQL、对象映射需配置 传统企业、高并发系统
Spring Data JPA 开发快、CRUD 自动生成 复杂查询难优化、N+1 问题 快速原型、内部工具
JDBC Template 极简、无黑盒 重复代码多、无动态 SQL 简单脚本、一次性任务
Python SQLAlchemy 表达力强、生态好 JVM 体系外、运维成本高 数据分析、AI 后台

我们团队最终选择 MyBatis,不是因为它最先进,而是最符合传统企业的“保守+可控”文化。领导不怕你写得慢,就怕你搞出个线上事故。


结语:工具没有银弹,只有合适

写到这里,我已经在工位上坐了 10 个小时。窗外北京的夜色沉沉,隔壁工位的同事还在和测试吵架:“这个 Bug 真不是我的问题,是 MyBatis 返回了 null!”

其实哪有什么框架原罪?MyBatis 只是一个工具。它不会替你写正确的 SQL,也不会帮你设计合理的表结构。但在传统企业数字化转型这条泥泞路上,它至少让我们这些 Java 后端,能在产品经理天马行空的需求和 DBA 铁面无私的规则之间,找到一丝喘息的空间。

如果你也在类似环境挣扎,别焦虑。把 MyBatis 用熟,把手写 SQL 练精,把慢查询日志盯紧——这些看似“土味”的技能,在跳槽时比什么“精通微服务”“熟悉云原生”都管用。

毕竟,能稳定跑在生产上的代码,才是好代码。至于区块链?等我把这个订单模块上线再说吧。

(完)

附:快速检查清单(上线前必看)

  • 所有 SQL 使用 #{} 而非 ${}
  • 动态查询测试了空参数、全参数场景
  • 批量操作用了 ExecutorType.BATCH
  • Mapper 接口和 XML 的 namespace 一致
  • 未开启生产环境 SQL 日志
  • 关联查询避免 N+1(用 IN 或 JOIN)
  • 字段名和 Java 属性名做了驼峰转换(mapUnderscoreToCamelCase: true

祝大家少加班,多涨薪。下次见。

评论 0

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