MyBatis基础教程:Java持久层框架入门

联调修仙者
2025-12-16 07:53
阅读 761

上周五晚上十点半,我瘫在工位上,盯着屏幕上又一个 NullPointerException,心里默念:“这破需求明天上线,产品经理今天下午才改完 PRD,测试同学已经开始冒烟了……” 我们小厂就这点不好——人少事多锅大。我是后端开发,独立负责一条核心业务线,从接口设计、数据库建模到部署上线基本都得自己兜底。坐标北京,每天通勤一小时,到家基本不想动,但最近为了准备跳槽(简历上总不能只写“会 CRUD 吧?”),硬着头皮把 MyBatis 重新啃了一遍。

说来惭愧,其实我之前一直用 Spring Data JPA + Hibernate,觉得自动生成 SQL 真香。但去年双11期间,我们搞了个数据同步服务,要从合作方的 API 拉取商品信息,结果发现 Hibernate 生成的 SQL 嵌套太深,慢得像蜗牛爬。运维同事半夜打电话:“你那个接口 QPS 才 50,DB CPU 快 100% 了!” 那一刻,我真想砸了电脑。

后来被领导“委婉建议”换成 MyBatis —— “你看隔壁 Python 组搞爬虫都比你快”。行吧,虽然我主业是 Java 后端,但为了保住饭碗,只能学。


为什么选 MyBatis?别再被“ORM 全自动”骗了

先说清楚,MyBatis 不是全自动 ORM,它是个半自动的持久层框架。什么意思?就是你得手写 SQL,但它帮你处理参数映射、结果集转换这些脏活累活。

对比一下:

  • Hibernate / JPA:你写个 findByUserIdAndStatus(),它自动生成 SQL。适合快速开发,但复杂查询或性能调优时,你根本不知道它到底干了啥。
  • MyBatis:SQL 你写,它执行。控制力强,性能可预测,尤其适合对数据库有深度优化需求的场景。

对于我们这种小厂来说,没有专职 DBA,SQL 写得好不好直接决定系统能不能扛住流量。而且我们有些接口要对接第三方数据源(比如从某宝爬商品信息——别问,问就是合法授权),字段不规整,JPA 的实体映射搞不定,MyBatis 直接写 SQL 就完事了。


实战:从零搭建一个商品同步服务

假设我们要做一个功能:定时从外部 API 拉取商品数据,存入本地 MySQL。这不是典型的“增删改查”,而是批量插入 + 更新,还涉及字段清洗、去重等逻辑。

第一步:项目初始化

我用的是 Spring Boot + MyBatis,依赖如下(pom.xml):

<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/product_db?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.demo.entity
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线字段自动转 Java 驼峰

注意:map-underscore-to-camel-case 这个配置超级实用!不然每次都要在 ResultMap 里手动写 column="create_time" property="createTime",烦死了。


第二步:建表 & 实体类

商品表结构(简化版):

CREATE TABLE `product` (
  `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
  `product_id` VARCHAR(64) NOT NULL UNIQUE,
  `title` VARCHAR(255),
  `price` DECIMAL(10,2),
  `status` TINYINT DEFAULT 1,
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
  `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

对应的 Java 实体:

public class Product {
    private Long id;
    private String productId;
    private String title;
    private BigDecimal price;
    private Integer status;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    // getter/setter 略
}

第三步:写 Mapper —— MyBatis 的灵魂

创建 ProductMapper.java 接口:

@Mapper
public interface ProductMapper {
    void batchInsert(@Param("products") List<Product> products);
    void batchUpdate(@Param("products") List<Product> products);
    List<Product> findByProductIdIn(@Param("productIds") List<String> productIds);
}

然后写 XML 映射文件 mapper/ProductMapper.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.demo.mapper.ProductMapper">

    <insert id="batchInsert" parameterType="list">
        INSERT INTO product (product_id, title, price, status)
        VALUES
        <foreach collection="products" item="item" separator=",">
            (#{item.productId}, #{item.title}, #{item.price}, #{item.status})
        </foreach>
        ON DUPLICATE KEY UPDATE
            title = VALUES(title),
            price = VALUES(price),
            update_time = NOW()
    </insert>

    <select id="findByProductIdIn" resultType="Product">
        SELECT * FROM product WHERE product_id IN
        <foreach collection="productIds" item="id" open="(" separator="," close=")">
            #{id}
        </foreach>
    </select>

</mapper>

这里有几个关键点:

  1. 批量插入 + ON DUPLICATE KEY UPDATE:这是 MySQL 特有的“存在则更新”语法,避免先查再插的 N+1 问题。
  2. <foreach> 标签:MyBatis 最常用的动态 SQL,处理 IN 查询、批量操作必备。
  3. parameterType="list":虽然写了,但其实可以省略,MyBatis 能自动推断。

⚠️ 踩坑提醒:
一开始我写 batchInsert 时没加 ON DUPLICATE KEY UPDATE,结果重复数据直接报主键冲突,任务直接挂掉。运维群里 @ 我:“又崩了?”,我只能苦笑:“在改,在改……”


性能优化:小厂也得讲武德

作为对性能有点执念的人,光跑通肯定不够。我们来看看怎么榨干 MyBatis 的性能。

1. 批量操作大小控制

不要一次性插 10 万条!MySQL 有 max_allowed_packet 限制,默认 64MB。我一般按 1000 条/批 切分:

public void syncProducts(List<Product> allProducts) {
    List<List<Product>> batches = Lists.partition(allProducts, 1000); // Guava 工具
    for (List<Product> batch : batches) {
        productMapper.batchInsert(batch);
    }
}

2. 使用 ExecutorType.BATCH

MyBatis 支持批处理模式,减少网络往返:

SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
try {
    ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
    for (Product p : products) {
        mapper.insert(p);
    }
    sqlSession.commit();
} finally {
    sqlSession.close();
}

不过要注意:Batch 模式不支持回滚单条记录,整个批次要么全成功,要么全失败。

3. 避免 N+1 查询

比如你要查订单和对应的商品,别在循环里查商品:

// ❌ 错误示范
for (Order order : orders) {
    Product p = productMapper.findById(order.getProductId()); // N次查询
}

应该用 JOININ 一次性查出:

// ✅ 正确做法
List<String> productIds = orders.stream().map(Order::getProductId).collect(Collectors.toList());
List<Product> products = productMapper.findByProductIdIn(productIds);

和 Python 爬虫组的“跨语言协作”

说到爬虫,我们公司有个小团队用 Python 写数据采集脚本(Scrapy + Requests),他们把原始 JSON 存到 MongoDB。我的任务是从 MongoDB 拉数据,清洗后存 MySQL。

一开始他们给的数据字段乱七八糟,比如价格有时是 "¥299",有时是 299.0,还有 null。我直接在 MyBatis 的 SQL 里做清洗:

<insert id="insertCleaned">
    INSERT INTO product (product_id, price)
    VALUES (
        #{productId},
        CASE
            WHEN #{rawPrice} REGEXP '^[0-9]+\.?[0-9]*$' THEN CAST(#{rawPrice} AS DECIMAL(10,2))
            ELSE 0.00
        END
    )
</insert>

Python 同学看了直呼内行:“原来 Java 也能这么灵活?” 其实这都是被逼的——没人帮我们做 ETL,后端就得顶上。


生产环境踩过的雷

雷区 1:SQL 注入?

很多人担心手写 SQL 会有注入风险。其实只要#{} 而不是 ${},MyBatis 会自动预编译,安全得很。

<!-- 安全 -->
SELECT * FROM user WHERE name = #{name}

<!-- 危险!可能注入 -->
SELECT * FROM user WHERE name = '${name}'

${} 只用于动态表名、排序字段等场景,且必须严格校验输入。

雷区 2:缓存失效

MyBatis 有一级(SqlSession 级)和二级(Mapper 级)缓存。但在分布式环境下,二级缓存容易导致脏读。我们直接关了:

<cache eviction="LRU" size="1024" readOnly="true" flushInterval="60000"/>

或者更干脆,在 application.yml 里全局关闭:

mybatis:
  configuration:
    cache-enabled: false

毕竟我们用 Redis 做应用层缓存,没必要让 MyBatis 搞一套。


写在最后:MyBatis 值得放进你的简历吗?

当然值得!

虽然现在流行“云原生”、“Serverless”,但数据库交互永远绕不开。MyBatis 作为国内 Java 后端的事实标准(看看 BAT 的招聘要求就知道),掌握它是基本功。

更重要的是,它逼你理解 SQL 本身。很多新人只会写 findAll(),遇到慢查询就懵。而 MyBatis 让你直面数据库,学会看执行计划、建索引、优化 JOIN —— 这些才是后端工程师的核心竞争力。

我现在简历上写的是:“熟练使用 MyBatis 实现高性能数据同步,QPS 提升 5 倍,支撑日均 500 万商品更新”。虽然有点吹,但面试官一听就知道你干过实事。


附:MyBatis vs JPA 快速对比表

维度 MyBatis Spring Data JPA
SQL 控制 完全掌控,手写 自动生成,黑盒
学习曲线 中等(需懂 SQL) 低(方法命名即可)
复杂查询 简单直接 需 JPQL / Criteria
性能调优 容易(SQL 可见) 困难(需查生成语句)
跨数据库 需写方言 自动适配
适合场景 高性能、复杂业务 快速原型、简单 CRUD

凌晨一点,终于把这篇文章写完。窗外北京的夜色依旧灯火通明,我知道明天还得改那个该死的分页接口——产品经理又说“能不能加个按销量排序”……

但没关系,至少现在我能用 MyBatis 写出高效的 SQL,而不是祈祷 Hibernate 别给我整出个笛卡尔积。

共勉,打工人。

评论 0

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