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

◆朱磊
2025-12-16 13:19
阅读 473

上周五晚上十点半,办公室只剩我一个人,盯着屏幕上 Caused by: org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectById 这行报错,差点把咖啡杯摔了。

说起来有点不好意思——我是去年才从传统制造业转行做 Java 开发的“高龄”程序员,今年刚好 30 岁。之前干的是机械设计,天天和 SolidWorks、AutoCAD 打交道,现在却在跟 Spring Boot、MySQL、Redis 死磕。好在我这人有个优点:死磕到底。尤其是最近准备跳槽,简历上得有像样的项目经验,光会写 CRUD 可不行。

事情是这样的:我们组接了个内部管理系统重构的活儿,老系统是用原生 JDBC 写的,SQL 散落在各个 Service 里,改个字段能累死人。产品经理还美其名曰“敏捷开发”,其实 deadline 比双十一抢购还紧。领导拍板:“上 MyBatis!别再手写 PreparedStatement 了,太原始了!”

于是我这个刚入行不到一年的“新人”被安排负责数据访问层的搭建。说实话,一开始我对 MyBatis 的理解仅限于“那个 XML 写 SQL 的框架”,连 #{}${} 的区别都说不清楚。但没办法,硬着头皮也得上。今天这篇笔记,就是我在踩坑、填坑、再踩坑之后,总结出来的一套 MyBatis 入门实战指南——尤其适合像我这样半路出家、又想搞懂底层原理的“转型选手”。


为什么不是 JPA?也不是 Python?

先说个题外话。我们团队里有个 Python 转过来的同事,他第一反应是:“为啥不用 SQLAlchemy 或者 Django ORM?”甚至私下问我:“你们 Java 非要搞这么复杂吗?”

我只能苦笑。Python 的 ORM 确实简洁优雅,写个模型类就能自动映射数据库,但在我们这种对 SQL 性能极度敏感 的企业级后端场景里,完全黑盒的 ORM 往往是性能杀手。比如一个复杂的多表关联查询,Django ORM 自动生成的 SQL 可能嵌套三层子查询,而 DBA 看了直接血压拉满。

MyBatis 的核心哲学就俩字:可控。它不替你生成 SQL,而是让你亲手写 SQL,再通过 XML 或注解把结果映射到 Java 对象。这听起来像是退步,但恰恰是大型项目里最需要的——你永远知道数据库到底执行了什么

而且,我们项目是纯 Java 技术栈(Spring Boot + MySQL + Redis),团队没人维护 Python 服务。强行引入 Python 不仅增加运维复杂度,还会让 CI/CD 流程分裂。所以,技术选型从来不只是“哪个更好”,而是“哪个更适合当前项目”


MyBatis 的架构思维:解耦 SQL 与逻辑

很多人学 MyBatis 只记住了怎么写 XML,但忽略了它的设计精髓将 SQL 语句与业务逻辑彻底分离

在老系统里,Service 层充斥着这样的代码:

String sql = "SELECT id, name, email FROM user WHERE status = ? AND create_time > ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setInt(1, 1);
    ps.setTimestamp(2, new Timestamp(System.currentTimeMillis() - 86400000));
    // ...一堆 set/get
}

这玩意儿的问题在哪?

  • SQL 和 Java 代码混在一起,改个条件就得重新编译
  • 无法复用 SQL
  • 测试困难(你总不能为了测一个 SQL 去启动整个应用)
  • DBA 想优化 SQL?先学会看 Java 代码吧!

而 MyBatis 通过 Mapper 接口 + XML/注解 的方式,把 SQL 抽离成独立资源。比如:

// UserMapper.java
public interface UserMapper {
    User selectById(Long id);
}
<!-- UserMapper.xml -->
<select id="selectById" resultType="User">
    SELECT id, name, email 
    FROM user 
    WHERE id = #{id}
</select>

接口定义契约,XML 实现细节。这种分层设计,让:

  • 开发专注业务逻辑(调用 Mapper 方法就行)
  • DBA 直接优化 XML 里的 SQL(无需碰 Java 代码)
  • 测试可以 mock Mapper(单元测试飞快)

这才是 MyBatis 在企业级项目中经久不衰的原因——它尊重专业分工


实战:从零搭建一个 MyBatis 项目

第一步:依赖配置(别漏了驱动!)

我第一次搭环境时,忘了加 MySQL 驱动,启动直接报 ClassNotFoundException: com.mysql.cj.jdbc.Driver。血泪教训:MyBatis 本身不包含数据库驱动

<!-- pom.xml -->
<dependencies>
    <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 -->
</dependencies>

第二步:核心配置文件 mybatis-config.xml

这是 MyBatis 的“中枢神经”,控制全局行为。我建议至少配这几项:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <!-- 类型别名,避免写全限定名 -->
    <typeAliases>
        <package name="com.example.model"/>
    </typeAliases>

    <!-- 环境配置 -->
    <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/myapp?useSSL=false"/>
                <property name="username" value="root"/>
                <property name="password" value="123456"/>
            </dataSource>
        </environment>
    </environments>

    <!-- 映射器注册 -->
    <mappers>
        <package name="com.example.mapper"/>
    </mappers>
</configuration>

💡 VSCode 小技巧:装个 MyBatis Code Helper 插件,XML 里写 #{} 会有智能提示,还能跳转到 Java 实体类。我装了十几个插件,就这个最实用。

第三步:写 Mapper 接口和 XML

这里有个经典坑点XML 文件必须和 Mapper 接口在同一个包下,且文件名一致!否则就会出现开头那个 BindingException

比如:

  • 接口路径:src/main/java/com/example/mapper/UserMapper.java
  • XML 路径:src/main/resources/com/example/mapper/UserMapper.xml

Maven 默认不会把 resources 下的非标准目录打包,记得在 pom.xml 里加 resource 配置:

<build>
    <resources>
        <resource>
            <directory>src/main/resources</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
    </resources>
</build>

第四步:动态 SQL —— MyBatis 的灵魂

MyBatis 最强大的地方不是简单查询,而是动态 SQL。比如实现一个用户搜索接口,条件可能包括 name、email、status,但前端可能只传其中几个。

不用 MyBatis 时,你会写一堆 if-else 拼接 SQL,极易出错。而 MyBatis 提供 <if>, <choose>, <foreach> 等标签:

<select id="searchUsers" resultType="User">
    SELECT id, name, email, status
    FROM user
    WHERE 1=1
    <if test="name != null and name != ''">
        AND name LIKE CONCAT('%', #{name}, '%')
    </if>
    <if test="email != null and email != ''">
        AND email = #{email}
    </if>
    <if test="statusList != null and statusList.size() > 0">
        AND status IN
        <foreach collection="statusList" item="status" open="(" separator="," close=")">
            #{status}
        </foreach>
    </if>
</select>

注意:

  • WHERE 1=1 是为了防止第一个条件不成立时 SQL 语法错误(虽然有点 hack,但有效)
  • LIKE 查询用 CONCAT 避免 SQL 注入(绝对不要用 ${} 拼接!

性能与安全:生产环境必须考虑的点

1. #{} vs ${} —— 别再问了!

  • #{value}:预编译占位符,自动防 SQL 注入,推荐 99% 场景使用
  • ${value}:直接字符串替换,有注入风险,仅用于动态表名、列名等(需手动校验)
<!-- 危险! -->
<select id="selectByColumn" resultType="User">
    SELECT * FROM user WHERE ${column} = #{value}
</select>

<!-- 安全做法:白名单校验 -->
public List<User> selectByColumn(String column, String value) {
    if (!Arrays.asList("name", "email").contains(column)) {
        throw new IllegalArgumentException("Invalid column");
    }
    return userMapper.selectByColumn(column, value);
}

2. 一级缓存 vs 二级缓存

MyBatis 默认开启一级缓存(SqlSession 级别),同一个 SqlSession 中重复查询会命中缓存。但 Spring Boot 中每次请求都会新建 SqlSession,所以一级缓存基本无效。

二级缓存(Mapper 级别)可以跨 SqlSession,但慎用!因为:

  • 缓存粒度粗,更新一条记录会导致整个 Mapper 缓存失效
  • 分布式环境下需要额外集成 Redis 等外部缓存

我的建议:除非读多写少且数据允许短暂不一致,否则别开二级缓存。我们项目直接关了:

<settings>
    <setting name="cacheEnabled" value="false"/>
</settings>

3. 批量操作优化

如果要插入 1000 条数据,千万别循环调用 insert!MyBatis 提供 <foreach> 批量插入:

<insert id="batchInsert">
    INSERT INTO user (name, email) VALUES
    <foreach collection="list" item="user" separator=",">
        (#{user.name}, #{user.email})
    </foreach>
</insert>

配合 JDBC 的 rewriteBatchedStatements=true 参数,性能提升 10 倍以上:

jdbc:mysql://localhost:3306/myapp?rewriteBatchedStatements=true

和 Spring Boot 整合:别自己造轮子

虽然上面演示了原生 MyBatis,但实际项目几乎都用 MyBatis-Spring-Boot-Starter。它自动配置了 SqlSessionFactory、MapperScanner,你只需要:

  1. 添加依赖
  2. application.yml
  3. 在 Mapper 接口上加 @Mapper 注解(或主类加 @MapperScan
# application.yml
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.model
  configuration:
    map-underscore-to-camel-case: true  # 数据库下划线自动转驼峰

这样,你就可以在 Service 里直接 @Autowired UserMapper,完全不用管 SqlSession 的创建和关闭——Spring 帮你管理了事务和资源。


总结:MyBatis 不是银弹,但值得掌握

从制造业转行这一年,我深刻体会到:没有完美的技术,只有合适的技术。MyBatis 学习曲线比 JPA 陡峭,但它给你的控制力和透明度,在复杂业务场景中无可替代。

上周,我终于把新系统的数据层上线了。虽然过程中被 BindingException 折磨到怀疑人生,但看到 DBA 赞许地说“这次 SQL 写得挺规范”,感觉一切都值了。

如果你也在准备跳槽、刷题、研究底层,MyBatis 绝对是 Java 后端面试的必考点。别只背八股文,去理解它的设计哲学——解耦、可控、灵活。这些思想,比框架本身更重要。

最后,送一句我们组长的话:“写代码不是为了跑通,是为了让人看懂、让机器高效。

共勉。


附:MyBatis 核心配置速查表

配置项 说明 推荐值
mapUnderscoreToCamelCase 数据库下划线字段自动映射为 Java 驼峰属性 true
cacheEnabled 是否启用二级缓存 false(除非明确需要)
logImpl 日志实现 SLF4J(配合 Logback)
jdbcTypeForNull NULL 值的 JDBC 类型 VARCHAR
defaultExecutorType 执行器类型 REUSE(可复用 PreparedStatement)

避坑清单

  • XML 文件路径必须和 Mapper 接口包路径一致
  • 别用 ${} 拼接用户输入
  • 批量操作务必用 <foreach> + rewriteBatchedStatements
  • 动态 SQL 的 WHERE 1=1 虽丑但实用(或用 <where> 标签)
  • 生产环境关闭二级缓存,除非你清楚后果

评论 0

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