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

写码不秃头
2025-12-12 20:23
阅读 329

上周五晚上 9 点,我正盯着屏幕上一行行红色的报错信息发呆——org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.example.mapper.UserMapper.selectById。那一刻我真的想砸电脑。

但不行啊,下周一就要提测了,而我还得在周末抽空刷行测题(别忘了,我是个正在备考公务员的在职程序员)。自从两个月前跳槽到这家新公司,项目技术栈从我熟悉的 Python/Go 切到了纯 Java 生态,MyBatis 就成了绕不开的一道坎。

说实话,在上一家公司写 Python 的时候,用 SQLAlchemy 或 Django ORM 简直是“所见即所得”;Go 里虽然没有那么高级的 ORM,但 GORM 也足够直白。可 MyBatis?它既不是全自动 ORM,又不像 JDBC 那么裸奔,属于那种“半自动”的中间态——听起来很美好,上手就懵逼。

为啥非得用 MyBatis?

我们组的技术栈比较传统:Spring Boot + MyBatis + MySQL。领导说这是“稳定、可控、便于 DBA 审核 SQL”。产品经理倒是开心了,因为每次改个字段他都能拉着 DBA 开会两小时,美其名曰“数据治理”。

其实我也理解,毕竟我们系统对接的是政务平台,SQL 必须清晰可审计,不能靠 ORM 自动生成一堆黑盒查询。所以 MyBatis 这种“手写 SQL + 自动映射”的模式,反而成了最佳选择。

但问题来了:作为一个刚入职两个月的“新人”,连 mapper.xml 放哪儿都不知道,更别说搞懂 #{}${} 的区别了。好在我有我的“外挂”——Claude 和 ChatGPT。每天下班前我都会把当天卡住的问题丢给它们,再结合《MyBatis 从入门到精通》这本书(对,就是那本封面像教科书的),一点点啃。

今天这篇技术分享,就是想把我踩过的坑、悟出的经验,用最直白的方式讲清楚。不为别的,就为了下次加班时,能少看一眼 Stack Overflow。


第一步:让 MyBatis 跑起来(别笑,很多人卡在这)

先说环境。我们的项目是 Spring Boot 3.x,用的是 MyBatis-Spring-Boot-Starter。依赖如下:

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

然后配置 application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_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.model
  configuration:
    map-underscore-to-camel-case: true

重点来了mapper-locations 这个配置,决定了 MyBatis 去哪找你的 XML 文件。我第一天就漏了这行,结果启动时报 Invalid bound statement,折腾了俩小时才发现是路径没配。

另外,map-underscore-to-camel-case: true 是神器!数据库字段一般是 user_name,Java 实体类是 userName,开这个配置就自动映射,省得你写 @Results


写个最简单的 CRUD

假设有个用户表:

CREATE TABLE user (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_name VARCHAR(50),
    email VARCHAR(100)
);

对应的 Java 实体:

public class User {
    private Long id;
    private String userName;
    private String email;
    // getter/setter 省略
}

然后是 Mapper 接口:

@Mapper
public interface UserMapper {
    User selectById(Long id);
    void insert(User user);
}

注意:接口上要加 @Mapper 注解,否则 Spring 找不到 Bean。或者你也可以在启动类加 @MapperScan("com.example.mapper"),批量扫描。

接着是 UserMapper.xml(放在 resources/mapper/ 下):

<?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="User" parameterType="long">
        SELECT id, user_name, email FROM user WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="User" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user (user_name, email) VALUES (#{userName}, #{email})
    </insert>

</mapper>

这里有几个关键点:

  • namespace 必须和接口全限定名一致;
  • resultType="User" 能直接写类名,是因为前面配了 type-aliases-package
  • useGeneratedKeys="true" 是为了获取自增主键,keyProperty="id" 表示把生成的 ID 回填到 user.id 字段。

我当时第一次写 insert 时忘了 useGeneratedKeys,结果 user.getId() 一直是 null,还以为是事务没提交……后来查日志发现 SQL 根本没返回 ID。


动态 SQL:比写 Python 字符串还爽?

MyBatis 最让我惊艳的,其实是它的动态 SQL。比如我要根据条件查用户:

<select id="searchUsers" resultType="User">
    SELECT id, user_name, email FROM user
    <where>
        <if test="userName != null and userName != ''">
            AND user_name LIKE CONCAT('%', #{userName}, '%')
        </if>
        <if test="email != null and email != ''">
            AND email = #{email}
        </if>
    </where>
</select>

<where> 标签会自动去掉开头的 AND,避免语法错误。这比我在 Go 里拼字符串安全多了——再也不用担心 SQL 注入(只要别手贱用 ${})。

说到 ${}#{} 的区别:

语法 作用 风险
#{value} 预编译占位符,等价于 ? 安全
${value} 直接字符串替换 高危!易导致 SQL 注入

除非你要动态拼表名或列名(比如分表),否则永远用 #{}。有一次我手滑写了 ${id},测试直接报“SQL 注入漏洞”,被安全团队追着问了一天……


性能与可维护性:我的执念

作为一个注重代码可读性的人,我不喜欢把 SQL 全塞进 XML 里搞得乱七八糟。所以我们团队约定:

  • 简单查询用 XML;
  • 复杂逻辑(比如多表关联、分页聚合)封装成视图或存储过程;
  • 所有 SQL 必须加注释,说明业务场景。

比如:

<!-- 查询近30天活跃用户,用于首页推荐 -->
<select id="findActiveUsers" resultType="User">
    SELECT u.* FROM user u
    INNER JOIN login_log l ON u.id = l.user_id
    WHERE l.login_time >= DATE_SUB(NOW(), INTERVAL 30 DAY)
    GROUP BY u.id
</select>

另外,MyBatis 的二级缓存默认是关闭的。我们线上开了 Redis 做分布式缓存,MyBatis 只做一级缓存(SqlSession 级别)。毕竟缓存一致性是个大坑,宁可慢一点,也不能脏读。


对比其他语言生态

说到这儿,忍不住对比下我熟悉的 Python 和 Go:

特性 MyBatis (Java) SQLAlchemy (Python) GORM (Go)
映射方式 手写 SQL + 自动映射 全自动生成或手写 结构体标签 + 方法链
学习曲线 中高(XML + 注解混合)
SQL 控制力 ⭐⭐⭐⭐⭐ ⭐⭐⭐(可通过 text()) ⭐⭐⭐⭐
调试难度 高(XML 路径、命名空间易错)

MyBatis 的优势在于对 SQL 的完全掌控,适合强监管、高合规的场景(比如政务、金融)。而 Python/Go 的 ORM 更适合快速迭代的互联网产品。

不过说实话,如果让我重新选,我还是更喜欢写 Python——毕竟 User.query.filter(User.name.like('%张%')) 比写 XML 爽多了。但考公路上,稳定压倒一切,Java 生态的“笨重”反而成了优点。


最后:给 fellow 程序员的建议

如果你和我一样,刚接触 MyBatis,记住三点:

  1. XML 路径和命名空间是玄学,配错就报 BindingException,仔细检查;
  2. 永远用 #{},除非你真的知道自己在干嘛
  3. 开启 MyBatis 日志:在 application.yml 加:
    logging:
      level:
        com.example.mapper: debug
    
    这样能看到实际执行的 SQL 和参数,debug 效率翻倍。

至于我?今晚还得继续刷行测。白天写 Java,晚上背“言语理解”,感觉自己像个分裂的赛博格。但没办法,程序员这行太卷了,考公至少是个退路。

希望这篇分享能帮你少踩几个坑。如果对你有用,欢迎点赞转发——毕竟,每一个还在加班写 CRUD 的程序员,都值得被世界温柔以待

P.S. 那本《MyBatis 从入门到精通》,我翻烂了前三章,后面基本靠官方文档和 AI。书籍适合打基础,实战还得靠项目喂。共勉!

评论 0

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