MyBatis初体验:一个前端仔被迫写Java后端的血泪史

工单终结者
2026-05-04 14:37
阅读 585

大家好,我是小林,深圳某腾讯系公司刚入职三个月的大专应届生。白天写Vue3组件,晚上啃Spring Boot,周末还得被领导拉着学AI——别问,问就是“全栈是趋势”。上周五晚上十点半,我正美滋滋准备下班,产品突然甩过来一个需求:“这个数据导出接口要支持动态SQL筛选,明天上线。”
我内心OS:???你当我是哆啦A梦啊?

更要命的是,后端同事全在搞大模型微调(没错,我们组最近全员卷AI),没人搭理我。没办法,硬着头皮自己上。翻了翻项目代码,发现用的是MyBatis——这玩意儿我只在B站教程里见过名字。于是那个周末,我泡在深圳湾人才公园旁的咖啡馆,从早八点肝到晚十点,总算把MyBatis基础摸了个七七八八。

今天这篇教程,就是给和我一样半路出家、被迫写Java后端的“前端难民”准备的。不讲虚的,直接上实战案例,带你从零跑通一个MyBatis CRUD接口。


为什么选MyBatis?因为我不想写JDBC

说实话,一开始我以为Java操作数据库就是写PreparedStatementResultSet那一套。直到看到我们老项目里那段300行的DAO层代码,里面全是手动拼SQL、手动映射字段、手动关连接……当时真的想砸电脑。

MyBatis最大的好处是什么?它让你专注SQL本身,而不是那些重复又容易出错的模板代码。比如我要查用户信息,在MyBatis里只需要写:

<!-- UserMapper.xml -->
<select id="selectUserById" resultType="com.example.model.User">
    SELECT id, name, email, created_at 
    FROM users 
    WHERE id = #{id}
</select>

框架自动帮你:

  • 创建PreparedStatement
  • 设置参数
  • 执行查询
  • 把ResultSet映射成Java对象
  • 关闭资源

这不比手写JDBC香多了?尤其对我们这种非科班出身的人来说,省下的脑细胞能多活五年。


搭建第一个MyBatis项目(基于Spring Boot)

我们的项目结构是标准的Spring Boot + MyBatis组合。先看依赖:

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

然后配数据库连接(application.yml):

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myapp?useSSL=false&serverTimezone=UTC
    username: root
    password: your_password
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*.xml  # XML映射文件位置
  type-aliases-package: com.example.model   # 实体类包路径,简化resultType写法

接着定义实体类:

// User.java
public class User {
    private Long id;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    
    // getter/setter 省略(Lombok真香)
}

再写Mapper接口(注意:这是接口,不是实现类!):

// UserMapper.java
@Mapper
public interface UserMapper {
    User selectUserById(Long id);
    List<User> selectAllUsers();
    int insertUser(User user);
    int updateUser(User user);
    int deleteUser(Long id);
}

最后是XML映射文件(resources/mapper/UserMapper.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.mapper.UserMapper">
    <select id="selectUserById" resultType="User">
        SELECT id, name, email, created_at 
        FROM users 
        WHERE id = #{id}
    </select>

    <select id="selectAllUsers" resultType="User">
        SELECT id, name, email, created_at FROM users
    </select>

    <insert id="insertUser" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO users(name, email, created_at) 
        VALUES(#{name}, #{email}, #{createdAt})
    </insert>

    <update id="updateUser">
        UPDATE users 
        SET name = #{name}, email = #{email} 
        WHERE id = #{id}
    </update>

    <delete id="deleteUser">
        DELETE FROM users WHERE id = #{id}
    </delete>
</mapper>

💡 关键点解释

  • namespace 必须和Mapper接口全限定名一致
  • resultType="User" 能简写是因为配置了 type-aliases-package
  • <insert> 中的 useGeneratedKeys="true" 是为了获取自增主键

动态SQL:产品经理的“灵活需求”克星

回到开头那个血泪需求——动态筛选。产品经理说:“用户可能按姓名搜,也可能按邮箱搜,也可能都不填,也可能都填……”

这时候就得用MyBatis的动态SQL了。比如:

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

<where> 标签会智能处理AND/OR前缀,避免出现 WHERE AND ... 这种语法错误。实测有效,再也不用写一堆 StringUtils.isNotBlank() 判断了。

但更骚的操作是 <foreach> ——比如批量删除:

<delete id="deleteUsersByIds">
    DELETE FROM users 
    WHERE id IN
    <foreach item="id" collection="ids" open="(" separator="," close=")">
        #{id}
    </foreach>
</delete>

传入一个 List<Long> ids,就能生成 DELETE ... WHERE id IN (1,2,3)。线上跑过千万级数据,稳如老狗(当然记得加索引)。


避坑指南:这些雷我替你踩过了

1. 字段名和属性名不一致?

数据库用下划线(created_at),Java用驼峰(createdAt)。解决方法有两种:

  • 全局开启驼峰转换(推荐):
    mybatis:
      configuration:
        map-underscore-to-camel-case: true
    
  • 或者在SQL里手动起别名:SELECT created_at AS createdAt

2. 参数传多个怎么办?

别傻傻地写 selectUser(String name, String email)。要么封装成DTO,要么用@Param注解:

User selectUser(@Param("name") String name, @Param("email") String email);

XML里就能用 #{name}#{email}

3. 事务失效?

MyBatis本身不管理事务,靠Spring。记得在Service方法上加 @Transactional,而且必须是public方法!我曾经在一个private方法上加,结果事务完全没生效,测试环境删了5000条数据回滚不了……那天晚上加班到凌晨两点。


和AI结合?试试Prompt工程优化SQL

最近组里在搞AI提效,我也尝试用大模型辅助写SQL。比如给Claude这样的Prompt:

你是一个资深MySQL DBA。请根据以下需求生成MyBatis动态SQL:

  • 表名:orders
  • 字段:id, user_id, status, amount, created_at
  • 支持按user_id精确查询、status精确查询、amount范围查询(minAmount, maxAmount)、created_at范围查询
  • 注意防SQL注入,使用#{}而非${}
  • 返回完整的