MyBatis 持久层开发

本文介绍 MyBatis 持久层开发的核心用法,覆盖 MyBatis 基本概念、Spring Boot 集成、Mapper 接口、XML 映射文件、参数传递、#$ 的区别、ResultMap 和动态 SQL。文章帮助读者理解 Java 后端项目如何通过 MyBatis 完成数据库查询和数据映射。

第一章 什么是 MyBatis?

在传统的 Java Web 开发中,我们通常使用 JDBC 来连接数据库和执行 SQL,但原生 JDBC 存在很多痛点:

  • 繁杂的样板代码:需要手动获取和关闭数据库连接、创建 Statement。
  • 参数设置繁琐:需要手动将 Java 对象中的数据,逐个按索引设置到 SQL 语句(?)中。
  • 结果集解析痛苦:需要手动遍历 ResultSet,将每一行记录映射封装成 Java 对象。

MyBatis 是一款优秀的持久层框架,更准确地说属于 SQL 映射框架。它内部封装了 JDBC,使开发者只需要关注 SQL 语句本身,而不需要花费精力去处理繁杂的数据库连接及结果映射过程。 MyBatis 的核心思想是:将 SQL 语句配置在 XML 或注解中,通过映射机制把 Java 对象与 SQL 语句及结果集关联起来。


第二章 快速入门程序(基于 Spring Boot)

下面我们通过一个最基础的 Spring Boot 整合 MyBatis 的例子,带你快速体验开发流程。

2.1 引入依赖

pom.xml 中引入 MyBatis 官方提供的起步依赖和 MySQL 驱动。

XML
<!-- MyBatis Spring Boot 启动器 -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <scope>runtime</scope>
</dependency>

2.2 配置数据源

application.yml 中配置数据库连接信息。Spring Boot 默认集成并使用了高性能的 HikariCP 连接池。

YAML
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_name?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: root_password
    driver-class-name: com.mysql.cj.jdbc.Driver

2.3 准备实体类 (Entity)

创建一个与数据库表字段对应的 Java 类:

Java
public class User {
    private Integer id;
    private String name;
    private Integer age;
    // 省略 getter、setter 和 toString() 方法
}

2.4 定义 Mapper 接口

Mapper 接口相当于我们常说的 Dao 层。使用 @Mapper 注解将其交给 Spring 容器管理。

Java
@Mapper
public interface UserMapper {
    // #{id} 是 MyBatis 的参数占位符,它会自动将方法参数的值填充进来
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Integer id);

    @Options(useGeneratedKeys = true, keyProperty = "id")
    @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
    void insert(User user);

    @Update("UPDATE user SET age = #{age} WHERE id = #{id}")
    void update(User user);

    @Delete("DELETE FROM user WHERE id = #{id}")
    void delete(Integer id);
}

获取自增主键: 如果在插入数据之后想要立刻获取到数据库自动生成的主键 ID,可以使用 @Options(useGeneratedKeys = true, keyProperty = "id")

  • useGeneratedKeys = true:开启获取数据库生成的主键。
  • keyProperty = "id":将生成的主键值赋给传入对象(如 User 对象)的 id 属性。执行完 insert 后,可直接通过 user.getId() 拿到该主键。

2.5 调用与测试

在 Service 逻辑层或单元测试中,直接通过 @Autowired 注入 Mapper 即可使用:

Java
@SpringBootTest
public class UserMapperTest {
    @Autowired
    private UserMapper userMapper;

    @Test
    public void testFindById() {
        User user = userMapper.findById(1);
        System.out.println("查询到的用户:" + user);
    }
}

第三章 XML 映射文件详解

在入门程序中,我们使用了 @Select 等注解来编写 SQL。但当 SQL 语句非常长,或者需要多表关联、动态拼接(如多条件搜索)时,写在注解里就难以维护。因此,XML 映射才是 MyBatis 的灵魂和企业开发最常用的方式。

3.1 配置 XML 扫描路径

通常,我们将 XML 映射文件放在 src/main/resources/mapper 目录下。需要在 application.yml 中告诉 MyBatis 去哪里找它们:

YAML
mybatis:
  mapper-locations: classpath:mapper/*.xml
  # 开启实体类别名扫描,在 XML 中就不需要写 com.example.entity.User 这么长的全限定类名了,直接写 User 即可
  type-aliases-package: com.example.demo.entity

3.2 编写 XML 映射文件

新建 resources/mapper/UserMapper.xml

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">
  
<!-- namespace 必须与对应的 Mapper 接口全限定名完全一致 -->
<mapper namespace="com.example.demo.mapper.UserMapper">

    <!-- id 必须与接口中的方法名一致 -->
    <!-- resultType 指定返回结果的单条记录映射类型(使用了别名,直接写 User) -->
    <select id="findById" resultType="User">
        SELECT id, name, age FROM user WHERE id = #{id}
    </select>
    
    <!-- insert 时,配置 useGeneratedKeys="true" 和 keyProperty="id" 可以自动将数据库生成的自增 ID 回填给 User 对象 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        INSERT INTO user(name, age) VALUES(#{name}, #{age})
    </insert>

</mapper>

对应 Mapper 接口的方法只需声明即可,无需任何注解:

Java
@Mapper
public interface UserMapper {
    User findById(Integer id);
    void insert(User user);
}

第四章 参数传递与安全性(#$ 的区别)

在 MyBatis 中传递参数有两种符号:

  • #{} (预编译):相当于 JDBC 中的 ?。MyBatis 会使用 PreparedStatement 安全地设置参数,能有效防止 SQL 注入。绝大多数情况下(如 WHERE id = #{id})都应使用它。
  • ${} (字符串拼接):相当于直接把参数拼接到 SQL 语句中,存在 SQL 注入风险。
  • 使用场景:当需要动态传递表名、列名,或者动态指定排序字段时(例如:ORDER BY ${columnName})。这种场景不能用预编译占位符,但必须在后端代码中严格校验传入的值,避免安全漏洞。

第五章 ResultMap:解决字段不匹配与复杂查询

5.0 resultType 与 resultMap 的选择

5.0.1 什么是 resultType?

resultType 是 MyBatis 的“快捷映射”方式,适合以下简单场景:

  • 单表查询,数据库列名与实体类属性名一致(或通过开启全局驼峰映射 map-underscore-to-camel-case 自动转换)。
  • 返回基本类型或包装类型,如 IntegerStringMap<String,Object>
  • 支持配合全局 TypeHandler 实现简单的枚举和类型转换。

注意:它无法处理嵌套对象、集合关联(一对一、一对多)等复杂结构,也不能为特定列单独指定 TypeHandler

示例:

XML
<select id="findById" resultType="User">
    SELECT id, name, age FROM user WHERE id = #{id}
</select>

5.0.2 什么时候必须用 resultMap?

  • 列名与属性名无法直接对应(如 user_nameuserName 且未开启全局驼峰)。
  • 需要嵌套映射(一对一 <association>、一对多 <collection>)。
  • 需要为某些列局部指定类型转换(如指定特定的 TypeHandler 处理加解密字段)。
  • 需要复用映射规则,多处 SQL 共用同一套字段映射。
  • 需要控制延迟加载缓存优化id 标签可提升性能)。

一句话总结:

能用 resultType 绝不用 resultMap;一旦字段对不上或要嵌套,立刻换成 resultMap。

5.1 解决字段名与属性名不一致

当数据库字段名叫 create_time,而实体类属性名叫 createTime 时,MyBatis 默认是映射不上的。

快捷方案:在 application.yml 中开启驼峰命名自动映射:
mybatis.configuration.map-underscore-to-camel-case=true
灵活方案:使用 <resultMap> 手动建立映射关系:
XML
<resultMap id="UserResultMap" type="User">
    <!-- id 标签专门映射主键,有助于提高性能 -->
    <id property="id" column="id"/>
    <!-- result 标签映射普通字段 -->
    <result property="userName" column="user_name"/>
    <result property="createTime" column="create_time"/>
</resultMap>

<!-- 使用 resultMap 代替 resultType -->
<select id="findAll" resultMap="UserResultMap">
    SELECT id, user_name, create_time FROM user
</select>

5.2 注解式映射:@Results@Result

如果不使用 XML,也可以直接在 Mapper 接口的方法上通过注解完成结果集映射:

Java
@Results(id = "userResultMap", value = {
    @Result(property = "id", column = "id", id = true),         // id = true 标记主键
    @Result(property = "userName", column = "user_name"),
    @Result(property = "createTime", column = "create_time")
})
@Select("SELECT id, user_name, create_time FROM user WHERE id = #{id}")
User findById(Integer id);

@Results@Result 的核心属性:

注解属性说明
@Resultsid为该映射起个 ID,方便 @ResultMap 复用
@ResultpropertyJava 实体类的属性名
@Resultcolumn数据库字段名
@Resultid是否为数据库主键(有助于性能优化)
@Resultmany一对多关系映射(搭配 @Many
@Resultone一对一关系映射(搭配 @One

复用已定义的 @Results

@ResultMap 引用其他方法上定义的 @Results(通过 id 属性引用):

Java
// 先定义映射
@Results(id = "userMap", value = {
    @Result(property = "id", column = "id", id = true),
    @Result(property = "userName", column = "user_name")
})
@Select("SELECT * FROM user WHERE id = #{id}")
User findById(Integer id);

// 复用上面的映射,无需重复定义
@ResultMap("userMap")
@Select("SELECT * FROM user WHERE name LIKE CONCAT('%', #{name}, '%')")
List<User> searchByName(String name);
@Results vs XML <resultMap> 注解方式定义直观,适合简单映射;XML 方式支持更复杂的一对多、多对多关联,且可在多个 XML 映射文件中灵活引用。建议简单场景用注解,复杂关联用 XML。

5.3 复杂的多表关联查询

<resultMap> 常用于处理 一对一(<association>一对多(<collection> 的多表关联映射,将关联表的记录嵌套组装到复杂的 Java 对象树中。

1. 一对一映射 (<association>)

场景:一个用户(User)拥有一个身份证信息(IdCard)。 实体类:

Java
public class User {
    private Integer id;
    private String name;
    private IdCard idCard; // 嵌套关联对象
    // getters and setters...
}

public class IdCard {
    private Integer id;
    private String idNumber;
    // getters and setters...
}

XML 映射:

XML
<resultMap id="UserWithIdCardMap" type="User">
    <id property="id" column="user_id"/>
    <result property="name" column="user_name"/>
    <!-- association 映射一对一关系 -->
    <!-- property: User 实体中的属性名; javaType: 关联对象的类型 -->
    <association property="idCard" javaType="IdCard">
        <id property="id" column="card_id"/>
        <result property="idNumber" column="id_number"/>
    </association>
</resultMap>

<select id="findUserWithIdCard" resultMap="UserWithIdCardMap">
    SELECT 
        u.id as user_id, u.name as user_name,
        c.id as card_id, c.id_number as id_number
    FROM user u
    LEFT JOIN id_card c ON u.id = c.user_id
    WHERE u.id = #{id}
</select>

2. 一对多映射 (<collection>)

场景:一个部门(Department)包含多个员工(Employee)。 实体类:

Java
public class Department {
    private Integer id;
    private String deptName;
    private List<Employee> employees; // 嵌套集合
    // getters and setters...
}

public class Employee {
    private Integer id;
    private String empName;
    // getters and setters...
}

XML 映射:

XML
<resultMap id="DepartmentWithEmployeesMap" type="Department">
    <id property="id" column="dept_id"/>
    <result property="deptName" column="dept_name"/>
    <!-- collection 映射一对多关系 -->
    <!-- property: Department 实体中的集合属性名; ofType: 集合内元素的类型 -->
    <collection property="employees" ofType="Employee">
        <id property="id" column="emp_id"/>
        <result property="empName" column="emp_name"/>
    </collection>
</resultMap>

<select id="findDepartmentWithEmployees" resultMap="DepartmentWithEmployeesMap">
    SELECT 
        d.id as dept_id, d.dept_name as dept_name,
        e.id as emp_id, e.emp_name as emp_name
    FROM department d
    LEFT JOIN employee e ON d.id = e.dept_id
    WHERE d.id = #{id}
</select>

3. 注解方式实现复杂映射

同样,注解也可以实现复杂映射。以一对一为例,使用 @One@Many 配合嵌套查询(N+1 查询方式):

Java
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
    @Result(property = "id", column = "id", id = true),
    @Result(property = "name", column = "name"),
    // 嵌套查询 IdCard,将 user 表的 id 传递给 findIdCardByUserId 方法
    @Result(property = "idCard", column = "id", 
            one = @One(select = "com.example.mapper.IdCardMapper.findIdCardByUserId"))
})
User findUserWithIdCard(Integer id);
注意:注解的 @One@Many 通常采用“嵌套查询”的方式,会引发著名的 N+1 查询问题(执行 1 条主查询后,还需要额外执行 N 条子查询来加载关联数据,影响性能)。而在 XML 中使用的 <association><collection> 配合 JOIN 语句,可以实现一条 SQL 查出所有数据(嵌套结果映射),性能更好。这也是为什么复杂查询强烈推荐使用 XML 映射文件的原因。

第六章 动态 SQL(MyBatis 的核心杀手锏)

业务中常常遇到条件可选的搜索页面,此时 SQL 的 WHERE 条件是动态变化的。MyBatis 提供了强大的动态标签来实现这一点。

6.1 <if><where>

<where> 标签会智能地添加 WHERE 关键字,并且自动去除首个条件前多余的 ANDOR

XML
<select id="searchUsers" resultType="User">
    SELECT * FROM user
    <where>
        <!-- 如果 name 属性不为空,则拼接这段 SQL -->
        <if test="name != null and name != ''">
            AND name LIKE CONCAT('%', #{name}, '%')
        </if>
        <!-- 如果 age 不为空,则拼接这段 SQL -->
        <if test="age != null">
            AND age = #{age}
        </if>
    </where>
</select>

6.2 <set>

用于动态更新,自动添加 SET 关键字,并剔除末尾多余的逗号。

XML
<update id="updateSelective">
    UPDATE user
    <set>
        <if test="name != null">name = #{name},</if>
        <if test="age != null">age = #{age},</if>
    </set>
    WHERE id = #{id}
</update>

6.3 <foreach>

作用:遍历集合/数组,常用于批量操作或 IN 查询的动态 SQL 拼接。

核心属性含义

  • collection:被遍历的集合名称(例如传递的参数名叫 ids,这里就写 ids)。
  • item:集合遍历出来的元素/项(代表当前遍历到的那个变量)。
  • separator:每一次遍历使用的分隔符(可选,例如 IN (1, 2, 3) 中的 ,)。
  • open:遍历开始前拼接的片段(可选,例如 ()。
  • close:遍历结束后拼接的片段(可选,例如 ))。
XML
<!-- 假设接口传入的参数名为 ids: List<Integer> ids -->
<select id="findByIds" resultType="User">
    SELECT * FROM user WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

第七章 Spring 事务与 MyBatis 协作

在 Spring Boot 项目中,事务由 Spring 管理,MyBatis 的 Mapper@Transactional 方法内会自动使用同一条数据库连接,无需手动 SqlSession.commit()

Java
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
    public void createOrder(Order order) {
        orderMapper.insert(order);
        orderMapper.insertLog(...);
        // 任一步失败 → 全部回滚
    }
}

要点:

  • 事务注解应加在 Service 层,不要加在 MapperController
  • 避免在同一个 Service 内用 this.xxx() 调用另一个带 @Transactional 的方法(会失效)。

系统学习 ACID、隔离级别、传播行为与常见失效场景,请参阅 《Java 数据库事务实战》


第八章 总结与进阶建议

通过 MyBatis,我们彻底告别了硬编码 JDBC 的时代:

  1. 注解开发:适合极简的单表 CRUD,开发快速。
  2. XML 开发:适合复杂 SQL、超长 SQL、连表查询,易于集中管理、复用和排查问题。
  3. 动态 SQL:让多条件搜索、批量操作等业务逻辑在 SQL 层变得非常灵活且优雅。

进阶方向: 在实际的企业级开发中,我们往往会在 MyBatis 的基础上引入 MyBatis-Plus。MyBatis-Plus 封装了几乎所有的单表 CRUD 操作,让你连简单的 XML 都不用写,而把 XML 专门留给复杂的“多表联查”和“报表统计”。这也是为什么先学懂 MyBatis 原理,再去学习 MyBatis-Plus 才能做到“知其然并知其所以然”。