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

Rerank观察员
2025-12-17 01:06
阅读 768

上周五晚上十点半,我正窝在沙发里用 VSCode 撸 Rust,突然钉钉“叮”一声——产品经理发来消息:“亲,在吗?下周三前要上线一个新模块,得支持用户积分明细查询,接口 QPS 要撑住 2000。”
我盯着那条消息,手里的咖啡都凉了。又来? 上周不是刚搞完双11大促的库存扣减优化?算了,打工人命苦,只能认。

但说实话,这次需求其实挺简单:查数据库、分页返回、加点条件过滤。问题在于,我们系统是典型的 Java 后端架构,用的 Spring Boot + MyBatis。而我……最近沉迷 Rust,对 Java 的手感有点生疏了。再加上团队里新来了几个应届生,天天问我“MyBatis 是啥?为啥不用 JPA 或者直接写 JDBC?”——行吧,那就借这个机会,好好捋一捋 MyBatis 这个“老古董”,顺便写篇教程,也算给新人铺个路,也给自己复习一把。


为什么是 MyBatis?而不是 Hibernate、JPA,或者直接上 Python?

先别急着喷。我知道现在很多人吹 Python 多香,Django ORM 几行代码搞定 CRUD,连 SQL 都不用写。但现实是:我们公司核心交易系统是 Java 写的,DB 是 MySQL 5.7,线上跑了三年多,每天处理几千万笔订单。 这种场景下,换语言?等于自杀。

至于 JPA/Hibernate?确实优雅,对象关系映射(ORM)全自动,但“全自动”往往意味着“不可控”。比如一个 @OneToMany 注解,不小心就触发 N+1 查询,线上慢查询日志爆了你都不知道咋回事。而 MyBatis 呢?它不替你做决定,SQL 你写,结果怎么映射你也控制——透明、可控、性能可预测。对我们这种高并发、低延迟要求的系统来说,这太重要了。

再说回求职。最近帮 HR 筛简历,发现不少应届生只会在 Spring Boot 项目里用 JPA 快速生成 CRUD 接口,问底层怎么优化、如何避免 SQL 注入、分页怎么实现,一脸懵。反倒是那些能手写 MyBatis Mapper、知道 #{}${} 区别的候选人,面试通过率高得多。企业要的是能扛生产环境的人,不是只会搭脚手架的“玩具工程师”。


实战:从零搭建一个 MyBatis 项目(基于 Spring Boot)

咱们不玩虚的,直接上手。假设你要做一个用户积分流水查询接口,表结构大概是这样:

CREATE TABLE `user_points_log` (
  `id` BIGINT NOT NULL AUTO_INCREMENT,
  `user_id` BIGINT NOT NULL,
  `points` INT NOT NULL,
  `reason` VARCHAR(100) NOT NULL COMMENT '变动原因',
  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  INDEX `idx_user_id` (`user_id`)
);

第一步:依赖引入(Maven)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <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>
</dependencies>

💡 踩坑提示:MyBatis-Spring-Boot-Starter 的版本和 Spring Boot 版本强相关!我们之前有个同事用 Spring Boot 3.x 配了 2.x 的 starter,启动直接报 ClassNotFoundException,折腾到凌晨两点。血泪教训!

第二步:配置数据源(application.yml)

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml  # XML 文件位置
  type-aliases-package: com.yourcompany.model  # 实体类别名包
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线自动转 Java 驼峰

这里重点说一下 map-underscore-to-camel-case。我们 DB 字段习惯用 snake_case(比如 user_id),而 Java 对象用 camelCaseuserId)。开了这个配置,MyBatis 会自动映射,省得你每个字段写 @Results

第三步:写实体类和 Mapper 接口

// UserPointsLog.java
public class UserPointsLog {
    private Long id;
    private Long userId;
    private Integer points;
    private String reason;
    private LocalDateTime createdAt;
    
    // getter/setter 略
}
// UserPointsLogMapper.java
@Mapper
public interface UserPointsLogMapper {
    List<UserPointsLog> selectByUserId(@Param("userId") Long userId, 
                                      @Param("offset") int offset, 
                                      @Param("limit") int limit);
    
    int countByUserId(@Param("userId") Long userId);
}

注意两点:

  1. @Mapper 注解标记接口,Spring Boot 自动扫描。
  2. 参数用 @Param 显式命名,XML 里才能用 #{userId} 引用。否则多个参数会报错!

第四步:写 XML 映射文件(关键!)

<!-- resources/mapper/UserPointsLogMapper.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.yourcompany.mapper.UserPointsLogMapper">

    <select id="selectByUserId" resultType="UserPointsLog">
        SELECT id, user_id, points, reason, created_at
        FROM user_points_log
        WHERE user_id = #{userId}
        ORDER BY created_at DESC
        LIMIT #{offset}, #{limit}
    </select>

    <select id="countByUserId" resultType="int">
        SELECT COUNT(1)
        FROM user_points_log
        WHERE user_id = #{userId}
    </select>

</mapper>

看到没?SQL 就是原生 SQL,清晰、高效、无黑盒。而且你可以随时在 SQL 里加索引提示、强制走某个索引,甚至写复杂 JOIN——这才是生产环境需要的自由度


避坑指南:那些年我踩过的 MyBatis 坑

1. ${} vs #{} —— SQL 注入的生死线

新手最爱犯的错:为了“动态表名”或“动态排序字段”,直接用 ${} 拼接:

<!-- 千万别这么干! -->
ORDER BY ${sortField} ${sortOrder}

如果 sortField 来自前端传参,黑客传个 1; DROP TABLE user_points_log--,你的表就没了。#{} 会预编译,${} 是纯字符串替换! 安全做法:后端白名单校验字段名,或者用枚举限制。

2. 分页别自己手写 OFFSET/LIMIT

上面例子用了 LIMIT #{offset}, #{limit},但在大数据量下(比如 offset=100000),MySQL 会扫描前 10 万行再丢弃,性能极差。我们线上就因此在双11被报警轰炸过。

正确姿势:用 MyBatis-Plus 或 PageHelper 插件,它们底层用“记录上次最大 ID”的方式实现分页(游标分页),效率 O(1)。

3. 事务管理别忘加 @Transactional

有一次我写了个积分扣除逻辑,先查余额,再插入流水,最后更新用户总积分。结果没加事务,某次网络抖动导致只插入了流水没更新余额——用户凭空多了积分!测试小姐姐提 bug 时我脸都绿了。

@Service
public class PointsService {
    
    @Transactional(rollbackFor = Exception.class)
    public void deductPoints(Long userId, int points, String reason) {
        // 1. 查当前积分
        // 2. 插入流水
        // 3. 更新总积分
    }
}

记住:涉及多表写操作,必须加事务!


性能调优:MyBatis 在生产环境的实战经验

优化项 默认行为 生产建议
一级缓存 开启(SqlSession 级) 一般不用管,但注意循环内不要复用 SqlSession
二级缓存 关闭 谨慎开启!分布式环境下容易脏读,建议用 Redis 替代
批量插入 单条 INSERT foreach + ExecutorType.BATCH,速度提升 10 倍+
日志输出 关闭 开发开 log4j.logger.com.yourcompany.mapper=DEBUG,生产务必关

特别说下批量插入。比如我们要初始化 10 万条用户数据:

@Autowired
private SqlSessionFactory sqlSessionFactory;

public void batchInsert(List<UserPointsLog> logs) {
    try (SqlSession session = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
        UserPointsLogMapper mapper = session.getMapper(UserPointsLogMapper.class);
        for (UserPointsLog log : logs) {
            mapper.insert(log);
        }
        session.commit(); // 一次性提交
    }
}

配合 XML 里的 <insert>,效率吊打循环单条插入。


最后聊聊:MyBatis 还值得学吗?

前几天和组里一个想跳槽的同事聊天,他说:“现在都流行 Go/Rust 写微服务了,Java 是不是快凉了?” 我笑了笑,给他看了我们监控大盘——QPS 峰值 12 万,P99 延迟 45ms,GC 暂停平均 3ms。这套系统跑在 Java + MyBatis + MySQL 上,稳如老狗。

技术没有银弹,只有合适不合适。如果你目标是进大厂做后端,Java 生态依然是主流。而 MyBatis 作为国内使用最广的持久层框架(阿里系、腾讯系、字节都在用),掌握它不仅能让你写出高性能、可维护的代码,更是面试时的一块硬通货。

至于我?写完这篇教程,Rust 的学习计划又得推迟了。不过没关系,反正产品经理明天又要改需求了(笑)。

附:VSCode 插件推荐(MyBatis 开发必备)

  • MyBatisX:XML 和 Mapper 接口互相跳转
  • Rainbow Brackets:括号彩虹色,看嵌套 SQL 不眼花
  • Error Lens:实时显示编译错误,比 IDEA 还快

啊对了,如果你也在远程办公,记得每小时起来活动下——我上周坐太久,腰疼到差点叫救护车。程序员,保命要紧啊!

评论 0

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