从抵触AI写代码到真香:MyBatis基础教程,一个Vim党在杭州的持久层入门血泪史

Linux夜行者
2025-12-18 04:13
阅读 1058

“以前看到同事用Copilot自动生成DAO层代码,我嗤之以鼻:‘这不就是把XML换成人话?’
直到上个月被一个紧急需求逼到凌晨三点,才明白——能少写一行重复代码,都是对发际线的尊重。”

大家好,我是老K,坐标杭州未来科技城,一个常年窝在Vim里敲:%s/old/new/g的Java后端。虽然公司(某厂字头大厂)早就在推AI辅助编码,但我一直觉得“真正的程序员就该手撸SQL”。直到去年双11压测时,因为手写JDBC连接池配置漏了maxWait参数,导致订单服务雪崩——那一刻,我站在工位前盯着满屏的Too many connections日志,突然悟了:框架不是偷懒,是避免重复踩坑

最近在折腾Rust的同时,也重新审视了Java生态里那些“老古董”技术。MyBatis就是其中之一。别看它年纪大,但在杭州这边(尤其是阿里系、网易系),面试官问“你用过哪些ORM框架”时,如果你只说Hibernate,HR可能连简历都懒得筛。求职市场上,MyBatis几乎是Java岗的默认选项——毕竟,谁不想招个能直接优化SQL的人呢?

今天这篇,就带大家从零搞懂MyBatis的核心逻辑。不讲虚的,全是我在项目里实打实用过的东西,包括那些让我想砸键盘的坑。


起因:产品经理又改需求了

事情要从上周五说起。我们组在做一个内部运营后台,原本用Spring Data JPA跑得好好的。结果产品经理周五下午4:58在群里@我:“能不能加个模糊搜索+分页+按状态过滤的功能?明天上线!

我当时内心OS:你当数据库是你家后花园啊?随便翻土播种?但转念一想,JPA动态查询写起来确实反人类——Specification嵌套三层,调试时断点都不知道打哪。更别说生成的SQL又臭又长,DBA看了直摇头。

于是拍板:切MyBatis。原因很简单:

  • 动态SQL写起来像写原生SQL,产品经理改需求?改个<if>标签就行
  • 性能可控,不会出现N+1查询这种经典背锅场景
  • 公司老系统全用MyBatis,新员工入职第一天就要看Mapper.xml

MyBatis核心思想:SQL与代码解耦

很多人误以为MyBatis只是“把SQL塞进XML”,其实它的精髓在于 “你掌控SQL,框架处理映射”。对比一下传统JDBC和MyBatis的流程:

步骤 原生JDBC MyBatis
获取连接 手动管理DataSource 框架自动注入SqlSession
执行SQL PreparedStatement硬编码 XML/注解定义SQL模板
结果映射 手动resultSet.getString("xxx") 自动映射到POJO
异常处理 try-catch嵌套地狱 统一异常转换

举个最简单的例子。假设我们要查用户信息:

// User.java (POJO)
public class User {
    private Long id;
    private String name;
    private String email;
    // getter/setter省略
}

在MyBatis里,你只需要两步:

  1. 定义Mapper接口
  2. 写SQL映射文件
// UserMapper.java
public interface UserMapper {
    User selectById(Long id);
}
<!-- UserMapper.xml -->
<?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.model.User">
    SELECT id, name, email 
    FROM users 
    WHERE id = #{id}
  </select>
  
</mapper>

注意那个#{id}——这就是MyBatis防SQL注入的关键。它会预编译成?占位符,比JDBC的String.format安全一万倍。曾经有个实习生直接拼接WHERE id = ${id},结果测试环境被注入删库,还好是测试库...


配置踩坑:别让环境变量背锅

MyBatis本身轻量,但和Spring Boot集成时,配置项多得让人眼花。我之前就栽在一个低级错误上:

# application.yml
mybatis:
  mapper-locations: classpath*:mapper/*.xml  # 注意是classpath*,不是classpath!
  type-aliases-package: com.example.model
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线字段自动转驼峰

重点说说classpath*这个星号。如果你的mapper文件放在多个jar包里(比如common模块),必须加*,否则启动时会报Invalid bound statement (not found)。这错误信息简直反人类——明明XML路径没错,却提示找不到方法。

还有一次,线上环境突然所有查询变慢。排查半天发现是运维把mybatis.configuration.cache-enabled设成了true,而我们的表更新频繁,缓存反而成了性能杀手。生产环境关缓存,除非你确定数据几乎不变


动态SQL实战:产品经理的“小需求”

回到开头那个紧急需求。用MyBatis实现动态查询有多爽?

<select id="searchUsers" resultType="User">
  SELECT id, name, email, status
  FROM users
  <where>
    <if test="keyword != null and keyword != ''">
      AND (name LIKE CONCAT('%', #{keyword}, '%') 
           OR email LIKE CONCAT('%', #{keyword}, '%'))
    </if>
    <if test="status != null">
      AND status = #{status}
    </if>
  </where>
  ORDER BY id DESC
  LIMIT #{offset}, #{size}
</select>

注意<where>标签的智能处理:它会自动去掉第一个AND,避免语法错误。要是手写JDBC,光这个就得写一堆StringBuilder判断。

分页部分我用的是LIMIT #{offset}, #{size},但要注意——MyBatis本身不提供分页插件!这里我故意没用PageHelper,因为:

  1. 大表深度分页性能差(OFFSET 100000会扫描10万行)
  2. 我们产品要求“无限滚动”,改用游标分页(WHERE id > lastId

说到产品,不得不吐槽:他们总以为“加个搜索框”是五分钟的事,殊不知要考虑索引设计、ES同步、敏感词过滤... 下次再有人跟我说“简单改个需求”,我就把DBA拉进群


性能调优:别让N+1查询毁了你

MyBatis最大的陷阱是什么?关联查询的N+1问题

假设我们查订单,同时要加载每个订单的用户信息:

// Order.java
public class Order {
    private Long id;
    private Long userId;
    private User user; // 关联对象
}

如果这样写:

<resultMap id="OrderWithUser" type="Order">
  <id property="id" column="id"/>
  <result property="userId" column="user_id"/>
  <association property="user" javaType="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
  </association>
</resultMap>

<select id="selectOrdersWithUser" resultMap="OrderWithUser">
  SELECT o.id, o.user_id, u.name as user_name
  FROM orders o
  LEFT JOIN users u ON o.user_id = u.id
</select>

这没问题,一次查询搞定。但如果偷懒写成:

// 错误示范!
List<Order> orders = orderMapper.selectAll();
for (Order order : orders) {
    order.setUser(userMapper.selectById(order.getUserId())); // 每次循环都查DB!
}

100个订单就产生101次SQL查询!线上CPU直接飙到90%。我在压测报告里看到这代码时,差点把键盘扔出窗外

正确做法要么用JOIN,要么用MyBatis的@SelectProvider手写批量查询。记住:任何循环里调DAO的操作,都是定时炸弹


为什么Go程序员也在看MyBatis?

可能有人问:现在不是流行Go吗?为啥还学Java的ORM?

两点真相:

  1. 杭州大厂后端主力仍是Java。阿里系P7以下基本要求Java,网易游戏后端也是Java为主。跳槽时MyBatis是硬通货。
  2. Go的ORM(如GORM)设计思路和MyBatis一脉相承。比如GORM的Scopes动态查询,本质就是MyBatis的<if>标签。理解MyBatis,等于掌握ORM通用范式。

我最近研究Rust的Diesel ORM,发现它也在解决同样的问题:如何平衡SQL控制力与开发效率。语言会变,但持久层的核心矛盾永远存在


总结:框架是工具,人才是核心

从抵触到真香,我对MyBatis的态度转变其实反映了程序员的成长:

  • 新人追求“炫技”,觉得手写JDBC才够硬核
  • 老手追求“稳定”,知道框架帮你避开了多少坑

MyBatis不是银弹,但它给了你可控的抽象。你可以:

  • 在XML里写复杂SQL(DBA友好)
  • 用注解快速开发简单CRUD
  • 通过插件扩展(比如SQL打印、分页)

最后分享个心得:不要为了用框架而用框架。我们组有个原则——简单单表操作用Spring Data JPA,复杂查询上MyBatis。技术选型要看场景,不是站队。

对了,上周那个需求最终按时上线了。产品经理请我喝了杯瑞幸,说“下次还找你”。我笑着点头,心里默念:下次记得提前一周提需求,不然我就用Hibernate给你生成100个left join


附:MyBatis vs Hibernate 关键对比(杭州互联网公司视角)

维度 MyBatis Hibernate
学习曲线 低(会SQL就会用) 高(需理解Session、缓存机制)
SQL控制力 完全掌控,可优化到极致 黑盒,复杂查询难优化
动态查询 XML标签直观 Criteria API反人类
求职热度 阿里/网易/拼多多标配 外企、传统企业较多
适合场景 互联网高并发、复杂查询 快速原型、简单CRUD

本文代码已脱敏,但血泪教训真实。欢迎杭州同僚来茶水间battle Vim vs VS Code——不过先说好,我的.vimrc可是配了LSP的,Copilot?拿来吧你!

评论 0

最热最新
暂无评论
Linux夜行者Lv.1
0
影响力
0
文章
0
粉丝