被迫写Java后,我终于搞懂了MyBatis

日志观察员
2026-03-04 14:28
阅读 2019

上周五晚上十点,工位上只剩我和隔壁组的运维小哥还在对峙。他盯着我刚上线的接口报错日志,幽幽地说:“你这SQL注入漏洞,怕是连GPT-4o都救不了。”我默默掏出保温杯喝了一口枸杞水,心想:早知道入职新公司两个月就得从Vim里切出Java世界,还不如继续在前端圈里卷React hooks。

但现实是,作为团队里唯一一个“会点后端”的前端(其实只是刚学Node.js两个月),领导拍着我肩膀说:“你不是研究过分布式系统吗?来,这个订单服务重构交给你了,用MyBatis,别整那些花里胡哨的JPA。”

行吧,谁让我是那个“全栈”(其实是前后端都会一点,但都不精)的倒霉蛋。


为什么是MyBatis?

说实话,一开始我对Java持久层框架的认知还停留在“Hibernate很重,MyBatis很轻”这种模糊印象。直到真正上手,才发现MyBatis的“轻”不是指代码量少,而是控制权在你手里——SQL怎么写、缓存怎么配、事务怎么管,全都明明白白写在XML或注解里,不像某些ORM框架,魔法太多,debug时像在猜谜。

而且,我们团队的数据库设计偏传统,很多老表字段命名不规范(比如usr_nmecr8_tme),用全自动映射的框架反而更麻烦。MyBatis允许你手动写SQL,还能用<resultMap>做字段映射,简直救星。


三步上手:从零到能跑

第一步:依赖安排上

Maven项目里加个依赖就行(Gradle用户请自行翻译):

<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>

如果你用的是Spring Boot,直接加mybatis-spring-boot-starter更省事。

第二步:配置数据源和SqlSessionFactory

MyBatis的核心是SqlSessionFactory,它负责创建SqlSession,而SqlSession就是执行SQL的入口。手动配置有点啰嗦,但有助于理解原理:

// 手动初始化(不推荐生产用,但学习时很有用)
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

mybatis-config.xml里主要配数据库连接、别名、Mapper位置等:

<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/order_db"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/OrderMapper.xml"/>
    </mappers>
</configuration>

小贴士:生产环境千万别把密码写死在配置文件里!我们用的是K8s的Secret挂载+环境变量,运维小哥上次就是因为这事骂我“安全意识为零”。

第三步:写Mapper和SQL

假设有个Order实体:

public class Order {
    private Long id;
    private String orderNo; // 注意:数据库字段可能是 order_no
    private Date createTime;
    // getter/setter 省略
}

对应的Mapper接口:

public interface OrderMapper {
    Order selectById(Long id);
    List<Order> selectAll();
    void insert(Order order);
}

然后在OrderMapper.xml里写SQL:

<mapper namespace="com.example.mapper.OrderMapper">
    <resultMap id="OrderResultMap" type="Order">
        <id property="id" column="id"/>
        <result property="orderNo" column="order_no"/>
        <result property="createTime" column="create_time"/>
    </resultMap>

    <select id="selectById" resultMap="OrderResultMap">
        SELECT id, order_no, create_time FROM orders WHERE id = #{id}
    </select>

    <select id="selectAll" resultMap="OrderResultMap">
        SELECT * FROM orders
    </select>

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO orders (order_no, create_time)
        VALUES (#{orderNo}, #{createTime})
    </insert>
</mapper>

注意几个关键点:

  • resultMap 解决了字段名和属性名不一致的问题
  • useGeneratedKeys 让自增主键回填到对象里
  • #{} 是预编译参数,防SQL注入(那位运维小哥终于闭嘴了)

坑与避坑指南

1. 缓存陷阱:一级缓存太“聪明”

MyBatis默认开启一级缓存(SqlSession级别)。我在测试时发现:同一个SqlSession里查两次同一个ID,第二次居然不走数据库!当时以为是Bug,后来才知道是缓存生效了。

但在Web应用中,每个请求通常新建一个SqlSession,所以一级缓存基本没用。如果真要缓存,建议用二级缓存(需手动开启),但要小心脏读——尤其是在分布式系统里,多个服务实例共享数据库,本地缓存很容易不一致。

我们的做法是:禁用MyBatis二级缓存,改用Redis做分布式缓存,由业务层控制缓存策略。毕竟,一致性比性能更重要,除非你是做秒杀系统(那又是另一个故事了)。

2. 动态SQL:别被<if>绕晕

产品经理上周提了个需求:“支持按订单号、用户ID、时间范围组合查询”。我第一反应是写一堆if-else拼SQL,但MyBatis的<where><if>标签让这事变得优雅:

<select id="searchOrders" resultMap="OrderResultMap">
    SELECT * FROM orders
    <where>
        <if test="orderNo != null and orderNo != ''">
            AND order_no = #{orderNo}
        </if>
        <if test="userId != null">
            AND user_id = #{userId}
        </if>
        <if test="startTime != null">
            AND create_time >= #{startTime}
        </if>
    </where>
</select>

<where>会自动去掉第一个AND,再也不用担心SQL语法错误了。不过要注意:test表达式里判断字符串非空,得同时判null和空串,不然前端传个空字符串进来,SQL就变成AND order_no = '',可能查出一堆脏数据。

3. 批量操作:别用foreach硬刚

有次我需要批量插入1万条订单,直接写:

<insert id="batchInsert">
    INSERT INTO orders (order_no, create_time)
    VALUES
    <foreach collection="list" item="item" separator=",">
        (#{item.orderNo}, #{item.createTime})
    </foreach>
</insert>

结果数据库直接卡死。原因?MySQL单条SQL有长度限制,而且大事务会锁表。

后来改成分批提交(每500条一提交),配合ExecutorType.BATCH,性能提升十倍。不过现在我们更倾向用消息队列异步处理大批量任务——毕竟,系统稳定性比“一次搞定”更重要。


MyBatis vs 其他技术:一点个人思考

虽然我在学Go,也听说Go的gorm用起来很爽,但Java生态的成熟度还是碾压级的。MyBatis虽然要手写SQL,但换来的是极致的可控性——你可以优化每一条SQL,可以加Hint,可以分库分表(配合ShardingSphere),甚至可以在SQL里调用存储过程(虽然我不推荐)。

至于算法?MyBatis本身不涉及复杂算法,但它背后的反射机制、动态代理、缓存淘汰策略,其实都是经典算法的应用。比如它的TypeHandler用策略模式处理类型转换,SqlSession用工厂模式创建,这些设计思想比背LeetCode题更有工程价值。

另外,GPT-4o确实能帮我生成MyBatis的模板代码,但它不懂业务上下文。比如它不知道我们订单表的status字段用0/1/2表示,而不是枚举值;也不知道某些字段要脱敏。所以,AI是工具,不是替代品。


写在最后

从Vim里敲JavaScript,到IDEA里写Java,再到深夜和MyBatis的XML文件对线,这两个月的经历让我明白:所谓全栈,不是什么都会,而是在不同技术栈之间快速切换的能力

MyBatis不算难,但细节很多。它不像Node.js那样“自由”,也不像Go那样“简洁”,但它稳、可靠、可控——这正是企业级应用最需要的。

现在,我的订单服务已经稳定跑了两周,QPS 2000+,没崩。运维小哥终于对我露出了微笑。而我,也准备开始研究MyBatis-Plus了——毕竟,能少写一行XML,都是对头发的尊重。

(完)

评论 0

最热最新
暂无评论
日志观察员Lv.1
0
影响力
0
文章
0
粉丝