MyBatis 中有哪些配置文件

在 MyBatis 体系中,Mapper XMLMyBatis Config XML 是两类核心配置文件,但在 Spring Boot 开发中,MyBatis Config XML 几乎被 配置化属性也就是properties或者yaml配置文件 代替了

明确一下两类配置文件的分工

配置文件类型 核心定位 核心作用 类比
Mapper XML 业务 SQL 层配置 定义具体的 SQL 语句、参数映射、结果映射 业务代码(个性化)
MyBatis Config XML 全局核心配置层 配置 MyBatis 全局规则(数据源、缓存、插件等) 框架配置(通用规则)

Mapper XML

Mapper XML 是 MyBatis 业务 SQL 的载体,它是将 Java 你写的 Mapper 中的接口方法与 SQL 语句绑定的,做手写 SQL + 参数 / 结果映射的,一个 Mapper XML 中的逻辑对应一个 Mapper 接口类

在 Spring Boot 中,Mapper XML 的编写逻辑完全不变,仅需在 application.yml 中指定文件位置

1
2
mybatis:
mapper-locations: classpath:mapper/*.xml # 告诉Spring Boot去哪里找Mapper XML

MyBatis Config XML

非 Spring Boot 的原生 MyBatis 项目中,mybatis-config.xml 是 全局配置入口 配置文件,负责定义 MyBatis 运行的所有全局规则,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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>
<!-- 1. 环境配置(数据源、事务) -->
<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/mybatisdemo"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
</dataSource>
</environment>
</environments>

<!-- 2. 注册Mapper XML -->
<mappers>
<mapper resource="mapper/UserMapper.xml"/>
</mappers>

<!-- 3. 全局设置 -->
<settings>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>

<!-- 4. 类型别名 -->
<typeAliases>
<package name="hbnu.project.mybatisdemo.entity"/>
</typeAliases>

<!-- 5. 插件配置 -->
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"/>
</plugins>
</configuration>

Spring Boot 的核心思想是 约定大于配置+自动配置,所以,它将 mybatis-config.xml 中的所有配置项,拆解为 application.yml/application.properties 中的属性配置,这样无需再写 Config XML

  • <environments>就给spring.datasource.*,让 Spring Boot 统一管理数据源
  • <mappers>注册 Mapper,到 Spring Boot 的配置文件就是mybatis.mapper-locations: classpath:mapper/*.xml
  • <typeAliases>就是mybatis.type-aliases-package: hbnu.project.mybatisdemo.entity
  • 而 Spring Boot 的自动扫描,@MapperScan 替代 <mappers> 标签,自动扫描 Mapper 接口,无需手动注册。

基本上比较经常使用的就是这些,所以说,这个配置文件如何单独编写就不说了

Spring Boot 中是否完全不需要 mybatis-config.xml呢?

  • 非必要不使用

    如果非要用 mybatis-config.xml,只需在 application.yml 中指定文件路径即可,示例

    1
    2
    3
    mybatis:
    config-location: classpath:mybatis-config.xml # 指定全局配置文件路径
    mapper-locations: classpath:mapper/*.xml # 仍需指定Mapper XML路径

此时 mybatis-config.xml 中可保留 <plugins>/<typeHandlers> 等复杂配置,而数据源、别名等仍可复用 application.yml 的配置,优先级是mybatis-config.xml > application.yml

如何编写Mapper XML

MyBatis 的持久层代码如何写

MyBatis的XML方式需要两部分:

  • Mapper 接口:定义方法,告诉 MyBatis 你要做什么操作。
  • XML 映射文件:写具体的SQL语句,告诉 MyBatis 怎么操作。

然后要告诉 MyBatis 启动后去哪找 Mapper,添加@Mapper注解或者启动类上放@MapperScan("mapper包名")

@Mapper注解这是一个 MyBatis 的 Mapper 接口,类似@Repository,需要在容器中为它创建代理对象。如果不加这个注解,Spring 就不会扫描到这个接口,也就不会生成代理对象注册到 Spring 容器中,你后面就无法通过 @Autowired 注入它。

最后,要在 Spring 的配置文件中配置 MyBatis 的 XML 文件路径,找到了 Mapper 要去找对应的 XML 来了解要做什么操作

Mpper XML文件的结构

Mapper XML 是 MyBatis 中业务 SQL 与 Java Mapper 接口绑定的核心载体,其结构遵循严格的 DTD 约束,且每个标签都有明确的职责边界。

一个规范的 Mapper XML 遵循 固定头部 + 必要标签 的结构,所有内容嵌套在 <mapper> 根标签内,完整骨架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8" ?>
<!-- 1. DTD约束(固定格式,MyBatis 3.0 标准) -->
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- 2. 根标签:mapper,唯一入口 -->
<mapper namespace="hbnu.project.mybatisdemo.mapper.UserMapper">

<!-- 2.1 可选:SQL片段(复用) -->
<sql id="UserColumns">id, name, email, age</sql>

<!-- 2.2 可选:结果映射(核心) -->
<resultMap id="UserResultMap" type="hbnu.project.mybatisdemo.entity.User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<!-- 多表关联:association/collection -->
</resultMap>

<!-- 2.3 核心:SQL操作标签(增删改查) -->
<insert id="insertUser">...</insert>
<delete id="deleteUserById">...</delete>
<update id="updateUser">...</update>
<select id="findUserById">...</select>

<!-- 2.4 可选:缓存配置(二级缓存) -->
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

</mapper>
  • 固定头部

    XML 声明 + DTD 约束,这是 Mapper XML 的 语法校验基础

    1
    2
    3
    4
    <?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">
    • XML 声明version="1.0" 指定 XML 版本,encoding="UTF-8" 强制文件编码
    • DTD 约束:MyBatis 官方定义的标签规则,IDE 会根据这个约束提示标签和属性,且 MyBatis 解析时会校验语法(比如<resultMap>必须有id属性)。
  • 根标签:<mapper>

    <mapper> 是整个文件的根标签,唯一作用是 将 XML 与 Mapper 接口绑定,核心属性是 namespace

    1
    <mapper namespace="hbnu.project.mybatisdemo.mapper.UserMapper">
    • namespace 必须等于 Mapper 接口的全类名
    • 一个 Mapper XML 对应一个 Mapper 接口,namespace 是唯一标识,不能重复。
  • 基础复用标签

    <sql> 用于提取重复的 SQL 片段,通过 <include> 引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <!-- 定义SQL片段:id是唯一标识 -->
    <sql id="UserColumns">id, name, email, age</sql>

    <!-- 引用片段:refid对应sql的id -->
    <select id="findUserById" resultMap="UserResultMap">
    SELECT <include refid="UserColumns"/>
    FROM users
    WHERE id = #{id}
    </select>

    当然上面这个例子只是一个简单的字符替换,<sql> 也可以使用带参数的 SQL 片段

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <sql id="TablePrefix">
    ${prefix}_users <!-- 动态表名,仅示例,慎用${} -->
    </sql>

    <select id="findUserByTable" resultMap="UserResultMap">
    SELECT <include refid="UserColumns"/>
    FROM <include refid="TablePrefix"/>
    WHERE id = #{id}
    </select>
    • <sql>id在当前 XML 中唯一即可
    • <include> 可嵌套,也可通过property传参,当然这就是比较复杂的写法了,能别这么写还是别这么写,太难看
  • 核心映射标签:<resultMap>

    <resultMap> 是 MyBatis 解决 数据库列名与 Java 实体属性名不一致 的标签,也是多表关联查询的关键,算是最重要的一个标签了

    基础结构

    1
    2
    3
    4
    5
    6
    7
    8
    <resultMap id="UserResultMap" type="hbnu.project.mybatisdemo.entity.User">
    <!-- 主键映射:必须用<id>,这是MyBatis优化缓存/嵌套查询的关键 -->
    <id column="id" property="id" jdbcType="INTEGER"/>
    <!-- 普通字段映射:<result> -->
    <result column="user_name" property="userName" jdbcType="VARCHAR"/>
    <result column="email" property="email" jdbcType="VARCHAR"/>
    <result column="age" property="age" jdbcType="INTEGER"/>
    </resultMap>
  • SQL 操作标签

    这是 Mapper XML 的最终目的,编写具体的数据库操作 SQL,所有标签都遵循「id绑定接口方法」的核心规则。

    例如新增标签<insert>

    1
    2
    3
    4
    5
    <insert id="insertUser" parameterType="User" 
    useGeneratedKeys="true" keyProperty="id" keyColumn="id">
    INSERT INTO users (name, email, age)
    VALUES (#{name}, #{email}, #{age})
    </insert>

    其中的属性:

    • useGeneratedKeys="true":开启自增主键回填(MySQL/Oracle 自增主键);
    • keyProperty="id":将生成的主键赋值到入参 User 对象的id属性;
    • keyColumn="id":数据库表的主键列名(与 keyProperty 对应,可选);
  • 可选标签:<cache>

    <cache> 用于开启当前 Mapper 的二级缓存,放在<mapper>根标签下:

    1
    2
    3
    4
    5
    <cache 
    eviction="LRU" <!-- 缓存淘汰策略LRU最近最少使用)/FIFO/SOFT/WEAK -->
    flushInterval="60000" <!-- 自动刷新时间(毫秒),60秒 -->
    size="512" <!-- 缓存最多存储512个对象 -->
    readOnly="true"/> <!-- 只读缓存(性能高,避免修改缓存) -->

    二级缓存默认关闭,需手动开启;而且开二级缓存,实体类必须实现Serializable接口,然后增删改操作会自动刷新缓存

<resultMap>标签详解

进一步认识<resultMap>标签

使用mybatis,有两个属性标签<resultType><resultMap>可以提供结果映射。

虽然<resultType> 属性在大部分情况下都够用,但是在一些特殊情况下无能为力,比如属性名和列名不一致,为一些连接的复杂语句编写映射代码。遇到这些情况,我们要使用<resultMap>标签

<resultMap> 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。

其中的属性如下

标签 属性 作用
<resultMap> id 当前 resultMap 的唯一标识,后续 SQL 标签用resultMap="UserResultMap"引用
type 映射的目标 Java 类,全类名 / 别名,别名需配置type-aliases-package
<id> column 数据库表的列名,大小写不敏感,与 SQL 查询的列名一致
property Java 实体类的属性名,大小写敏感,必须能与实体类 getter/setter对应上
jdbcType 数据库字段类型,可选,用于类型转换,如 VARCHAR/INTEGER/DATE
<result> <id> 普通字段映射,非主键

属性名和列名不一致是使用<resultMap>标签最常见的情况

  • 例如

    1
    2
    3
    4
    5
    public class User {
    private int id;
    private String username;
    private String hashedPassword;
    }
    1
    2
    3
    4
    5
    6
    <select id="selectUsers" resultType="User">
    select
    user_id, user_name, hashed_password
    from some_table
    where id = #{id}
    </select>

    为了 解决 上述问题,我们只需要在 <resultMap>中做一下简单的对应的配置

    1
    2
    3
    4
    5
    <resultMap id="userResultMap" type="User">
    <id property="id" column="user_id" />
    <result property="username" column="user_name"/>
    <result property="password" column="hashed_password"/>
    </resultMap>

    然后然后在引用它的语句中设置 <resultMap>属性就行了,去掉<reslutType>属性,用<resultMap>代替,二者只能选择其中的一个

    1
    2
    3
    4
    5
    <select id="selectUsers" resultMap="userResultMap">
    select user_id, user_name, hashed_password
    from some_table
    where id = #{id}
    </select>

那么,多表关联映射如何写呢?包括一对一映射,多对多映射

编写多表关联映射

无论哪种关联类型,MyBatis 多表映射的本质是:

  • 将多表查询的结果,通过 resultMap 标签的层级结构,赋值到 Java 实体的 基本属性 + 关联属性 中

所以说通用步骤是

  • 明确关联关系:先理清数据库表之间的关联字段
  • 定义实体关联属性:在主实体中添加关联对象 / 集合属性
  • 编写关联查询 SQL:根据需求选择联表还是分布
  • 配置 resultMap:用对应标签映射关联字段到实体属性。

日常开发中多表关联主要分 3 类,resultMap 针对不同场景提供了专属标签:

关联类型 业务场景 resultMap 核心标签 示例
一对一 用户 ↔︎ 用户详情(1:1) <association> User 包含 UserInfo
一对多 用户 ↔︎ 订单(1:N) <collection> User 包含List<Order>
多对多 用户 ↔︎ 角色(M:N) <collection> User 包含 List<Role>(通过中间表)

数据表和实体是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 用户主实体
@Data
public class User {
private Integer id;
private String name;
private String email;
private Integer age;

// 一对一关联:用户详情
private UserInfo userInfo;
// 一对多关联:用户订单
private List<Order> orders;
// 多对多关联:用户角色
private List<Role> roles;
}

// 用户详情(一对一)
@Data
public class UserInfo {
private Integer id;
private Integer userId;
private String phone;
private String address;
}

// 订单(一对多)
@Data
public class Order {
private Integer id;
private Integer userId;
private String orderNo;
private BigDecimal amount;
}

// 角色(多对多)
@Data
public class Role {
private Integer id;
private String roleName;
}
  • 一对一关联映射<association>

    一对一关联,例如 User ↔︎ UserInfo,使用 <association> 标签,它表示 当前实体包含一个关联实体对象,支持两种实现方式:

    我先说其中的参数

    属性 作用
    property 当前实体中关联对象的属性名(User 中的 userInfo
    javaType 关联对象的类型(全类名 / 别名,如 UserInfo
    column 数据库列名(联表查询时需注意列名冲突,用 AS 起别名,如 ui.id AS info_id
    select 关联查询的 Mapper 方法(全类名 + 方法名)
    fetchType 加载策略:lazy(懒加载,用到时才查)/eager(立即加载),默认 eager
    • 嵌套查询(分步查)

      对于这样的一个方法

      1
      2
      // UserMapper.java
      User findUserWithInfoById(Integer id);

      编写联表查询 SQL + resultMap,整体步骤如下

      • 先编写联表查询 SQL,别忘了处理列名冲突,若主表和关联表有同名列,给关联表的列起别名
      • 然后配置 resultMap,先映射主实体的基本属性,然后嵌套 <association> 标签,映射关联实体属性
      • 之后在 <select> 标签中,通过 resultMap 属性指定上述配置的 resultMap 名称。

      按这样步骤编写的例子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      <!-- UserMapper.xml -->
      <mapper namespace="hbnu.project.mybatisdemo.mapper.UserMapper">
      <!-- 定义一对一关联的resultMap -->
      <resultMap id="UserWithInfoResultMap" type="hbnu.project.mybatisdemo.entity.User">
      <!-- 映射用户主表字段 -->
      <id column="id" property="id"/>
      <result column="name" property="name"/>
      <result column="email" property="email"/>
      <result column="age" property="age"/>

      <!-- 一对一关联:UserInfo -->
      <association property="userInfo" javaType="hbnu.project.mybatisdemo.entity.UserInfo">
      <!-- 映射用户详情表字段(注意列名冲突,需别名) -->
      <id column="info_id" property="id"/>
      <result column="user_id" property="userId"/>
      <result column="phone" property="phone"/>
      <result column="address" property="address"/>
      </association>
      </resultMap>

      <!-- 联表查询SQL(users LEFT JOIN user_info) -->
      <select id="findUserWithInfoById" resultMap="UserWithInfoResultMap">
      SELECT
      u.id, u.name, u.email, u.age,
      ui.id AS info_id, ui.user_id, ui.phone, ui.address
      FROM users u
      LEFT JOIN user_info ui ON u.id = ui.user_id
      WHERE u.id = #{id}
      </select>
      </mapper>
    • 嵌套结果(联表查)。

      先查主表,再根据主表结果查关联表,对于上面同样的 Mapper 接口

      • 先编写 UserInfoMapper 查询方法

        1
        2
        3
        4
        5
        6
        <!-- UserInfoMapper.xml -->
        <mapper namespace="hbnu.project.mybatisdemo.mapper.UserInfoMapper">
        <select id="findUserInfoByUserId" parameterType="int" resultType="UserInfo">
        SELECT id, user_id, phone, address FROM user_info WHERE user_id = #{userId}
        </select>
        </mapper>
      • 然后在 UserMapper 中定义嵌套查询的 resultMap

        • 先映射主实体的基本属性
        • 嵌套 <association> 标签,配置分步查询参数
          • property:主实体中关联对象的属性名;
          • javaType:关联实体类型;
          • column:主表的关联字段(如 id,将主表查询结果的 id 传给关联查询);
          • select:关联查询的 Mapper 方法全路径(如 xxx.UserInfoMapper.findUserInfoByUserId);
          • 可选 fetchType="lazy":开启懒加载
        • 最后编写主表查询 SQL,仅查询主表字段,关联表数据通过 <association>select 分步查询。
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        <!-- UserMapper.xml -->
        <resultMap id="UserWithInfoNestedResultMap" type="User">
        <!-- 主表字段映射 -->
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="email" property="email"/>
        <result column="age" property="age"/>

        <!-- 一对一嵌套查询:先查用户,再查用户详情 -->
        <association
        property="userInfo"
        javaType="UserInfo"
        column="id"
        select="hbnu.project.mybatisdemo.mapper.UserInfoMapper.findUserInfoByUserId"/>
        </resultMap>

        <!-- 只查主表,关联表按需查询 -->
        <select id="findUserWithInfoNestedById" resultMap="UserWithInfoNestedResultMap">
        SELECT id, name, email, age FROM users WHERE id = #{id}
        </select>
  • 一对多关联映射<collection>

    一对多关联,就例如User ↔︎ List<Order>,使用 <collection> 标签,它表示 当前实体包含一个关联对象的集合,同样支持嵌套结果和嵌套查询。其实有些类似于一对一

    其中的参数如下

    属性 作用
    property 当前实体中集合属性名(User 中的 orders
    ofType 集合中元素的类型(Order,区别于 javaTypejavaType 是集合类型如 List)
    column 数据库列名(同样注意列名冲突,订单 id 起别名 order_id
    • 嵌套结果(联表查询)

      • 对于这样的一个 Mapper 接口方法

        1
        User findUserWithOrdersById(Integer id);
      • 编写联表查询 + resultMap,具体步骤如下

        • 编写联表查询 SQL,同样注意处理列名冲突
        • 配置 resultMap
          • 先映射主实体的基本属性,同一对一
          • 然后嵌套 <collection> 标签,映射关联集合:
            • property:主实体中集合属性名(如 User 的 orders);
            • ofType:集合中元素的类型(如 Order,注意不是 List);
            • <collection> 内部,用 <id>/<result> 映射关联表字段(别名)到集合元素的属性。
        • 绑定 SQL 与 resultMap

        这样的步骤编写出的例子如下

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        <!-- UserMapper.xml -->
        <resultMap id="UserWithOrdersResultMap" type="User">
        <!-- 主表字段映射 -->
        <id column="id" property="id"/>
        <result column="name" property="name"/>
        <result column="email" property="email"/>
        <result column="age" property="age"/>

        <!-- 一对多关联:List<Order> -->
        <collection
        property="orders"
        ofType="hbnu.project.mybatisdemo.entity.Order"> <!-- 集合中元素的类型 -->
        <id column="order_id" property="id"/>
        <result column="user_id" property="userId"/>
        <result column="order_no" property="orderNo"/>
        <result column="amount" property="amount"/>
        </collection>
        </resultMap>

        <!-- 联表查询:用户 + 订单 -->
        <select id="findUserWithOrdersById" resultMap="UserWithOrdersResultMap">
        SELECT
        u.id, u.name, u.email, u.age,
        o.id AS order_id, o.user_id, o.order_no, o.amount
        FROM users u
        LEFT JOIN orders o ON u.id = o.user_id
        WHERE u.id = #{id}
        </select>
    • 嵌套查询(分布加载)。步骤类似

      • 编写关联表的查询 Mapper
      • 配置主实体的 resultMap
      • 编写主表查询 SQL,仅查主表字段
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      <!-- OrderMapper.xml 先定义查询方法 -->
      <mapper namespace="hbnu.project.mybatisdemo.mapper.OrderMapper">
      <select id="findOrdersByUserId" parameterType="int" resultType="Order">
      SELECT id, user_id, order_no, amount FROM orders WHERE user_id = #{userId}
      </select>
      </mapper>

      <!-- UserMapper.xml 中嵌套查询 -->
      <resultMap id="UserWithOrdersNestedResultMap" type="User">
      <id column="id" property="id"/>
      <result column="name" property="name"/>
      <result column="email" property="email"/>
      <result column="age" property="age"/>

      <collection
      property="orders"
      ofType="Order"
      column="id" <!-- 传递用户id给订单查询 -->
      select="hbnu.project.mybatisdemo.mapper.OrderMapper.findOrdersByUserId"
      fetchType="lazy"/> <!-- 懒加载,用到订单时才查 -->
      </resultMap>

      <select id="findUserWithOrdersNestedById" resultMap="UserWithOrdersNestedResultMap">
      SELECT id, name, email, age FROM users WHERE id = #{id}
      </select>
  • 多对多关联映射(基于 <collection>

    多对多关联,例如User ↔︎ Role,本质是 一对多 + 中间表,通过中间表 user_role 联表查询,仍使用 <collection> 标签。

    多对多 = 「主表 ↔︎ 中间表 ↔︎ 关联表」的两次一对多,本质仍用 <collection> 映射集合属性。

    那么,具体实现的步骤如下

    • 编写三表联查 SQL:主表 + 中间表 + 关联表 JOIN,也别忘了处理列名冲突
    • 然后配置 resultMap
      • 先映射主实体的基本属性
      • 嵌套 <collection> 标签,映射关联角色集合,同一对多
    • 绑定 SQL 与 resultMap

    那么,例如,对于这样的一个 Mapper 方法

    1
    User findUserWithRolesById(Integer id);

    编写这样的resultMap

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    <!-- UserMapper.xml -->
    <resultMap id="UserWithRolesResultMap" type="User">
    <!-- 主表字段映射 -->
    <id column="id" property="id"/>
    <result column="name" property="name"/>
    <result column="email" property="email"/>
    <result column="age" property="age"/>

    <!-- 多对多关联:List<Role>(通过user_role中间表) -->
    <collection
    property="roles"
    ofType="hbnu.project.mybatisdemo.entity.Role">
    <id column="role_id" property="id"/>
    <result column="role_name" property="roleName"/>
    </collection>
    </resultMap>

    <!-- 联表查询:users ↔ user_role ↔ roles -->
    <select id="findUserWithRolesById" resultMap="UserWithRolesResultMap">
    SELECT
    u.id, u.name, u.email, u.age,
    r.id AS role_id, r.role_name
    FROM users u
    LEFT JOIN user_role ur ON u.id = ur.user_id
    LEFT JOIN roles r ON ur.role_id = r.id
    WHERE u.id = #{id}
    </select>

最后,一般来说,能用嵌套结果就用嵌套结果的方式,如果你用嵌套查询容易会触发 N+1 查询问题

fetchType="lazy"类似 Hibernate,是针对非核心关联数据,用到时才查询,减少初始查询的数据量;需开启 MyBatis 懒加载全局配置

1
2
3
4
mybatis:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: false # 关闭积极懒加载(仅加载指定属性)

resultType 替代 resultMap

如果数据库列名与实体属性名完全一致,或开启了驼峰转换map-underscore-to-camel-case: true,可直接用resultType,无需写<resultMap>

1
2
3
<select id="findUserById" parameterType="int" resultType="hbnu.project.mybatisdemo.entity.User">
SELECT id, name, email, age FROM users WHERE id = #{id}
</select>

一般情况下,这个东西好像只能是单表简单查询用,没见过多表关联用这个的

SQL操作标签详解

MyBatis 的 <insert><delete><update><select> 是映射文件中处理数据库增删改查的核心标签,也是日常开发中使用频率最高的组件。

在分别展开描述单个标签前,先掌握所有 SQL 操作标签的通用约束

通用规则 具体要求
核心绑定逻辑 标签 id 必须与 Mapper 接口方法名完全一致(大小写敏感),namespace 绑定接口全类名
参数绑定 #{} 做预编译参数绑定(防注入),${} 仅用于动态表名 / 列名
配置优先级 标签属性 > MyBatis 全局配置 > Spring Boot 配置
异常处理 增删改默认抛出 SQLExecutionException,查询抛出 NoResultException(可选)
缓存行为 增删改默认刷新二级缓存(flushCache=true),查询默认不刷新(flushCache=false

<insert>:新增标签

用于执行数据库 INSERT 语句。进行插入数据

他不仅解决数据插入,还会对主键进行回填,是业务中创建数据的入口。

其中的属性如下

属性名 作用 示例 是否必填
id 绑定 Mapper 接口的新增方法名 id="insertUser"
parameterType 入参类型(全类名 / 别名,MyBatis 可自动推断,建议显式声明) parameterType="User"
useGeneratedKeys 是否开启自增主键回填(适配 MySQL/AutoIncrement、PostgreSQL/Sequence) useGeneratedKeys="true"
keyProperty 主键回填到入参实体的属性名(需配合 useGeneratedKeys keyProperty="id"
keyColumn 数据库表的主键列名(与 keyProperty 对应,列名与属性名一致时可省略) keyColumn="user_id"
flushCache 是否刷新二级缓存(默认 true,增删改操作强制刷新) flushCache="true"
timeout SQL 执行超时时间(秒),超时抛出 SQLTimeoutException timeout="10"

插入单条数据并自动回填自增主键的例子就如下

1
2
3
4
5
6
7
8
<!-- UserMapper.xml -->
<insert id="insertUser"
parameterType="hbnu.project.mybatisdemo.entity.User"
useGeneratedKeys="true"
keyProperty="id">
INSERT INTO users (name, email, age)
VALUES (#{name}, #{email}, #{age})
</insert>

对应的 Mapper 接口

1
2
// 返回值为受影响行数,主键会回填到入参 User 对象的 id 属性
int insertUser(User user);

如果你的数据库不是 MySQL,是 PostgreSQL/Oracle 那种主键序列,可以自定义主键生成

1
2
3
4
5
6
7
8
<insert id="insertUserWithSequence">
<!-- selectKey:插入前生成主键,order="BEFORE" -->
<selectKey keyProperty="id" resultType="int" order="BEFORE">
SELECT nextval('users_id_seq')
</selectKey>
INSERT INTO users (id, name, email, age)
VALUES (#{id}, #{name}, #{email}, #{age})
</insert>
  • order="BEFORE":插入前执行主键查询,适配序列,order="AFTER"是插入后执行,适配 MySQL 自增

批量插入就这样

1
2
3
4
5
6
7
<insert id="batchInsertUser" parameterType="java.util.List">
INSERT INTO users (name, email, age)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.age})
</foreach>
</insert>

对应的接口如下

1
int batchInsertUser(List<User> userList);
  • <foreach> 遍历集合,拼接批量插入 SQL,避免循环单条插入

<delete>:删除标签

执行 DELETE 语句,能够解决 单条 或 批量 删除,别忘了删除条件,避免全表删除

参数比较少,也和上面的差不多

属性名 作用 示例
id 绑定 Mapper 接口删除方法名 id="deleteUserById"
parameterType 入参类型(int/List/Map/ 实体) parameterType="java.util.List"
timeout 执行超时时间 timeout="5"

按主键单条删除是最基础的用法

例如对应的接口:int deleteUserById(Integer id);

1
2
3
<delete id="deleteUserById" parameterType="int">
DELETE FROM users WHERE id = #{id}
</delete>

如果是条件删除,按非主键

1
2
3
<delete id="deleteUserByEmail" parameterType="java.lang.String">
DELETE FROM users WHERE email = #{email}
</delete>

批量删除也类似,使用<foreach>

1
2
3
4
5
6
<delete id="batchDeleteUser" parameterType="java.util.List">
DELETE FROM users WHERE id IN
<foreach collection="list" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

<update>:更新标签

执行 UPDATE 语句,也就是字段更新 ,主要是动态部分字段更新,是业务中修改数据的核心标签。

其中的属性如下

属性名 作用 示例
id 绑定 Mapper 接口更新方法名 id="updateUser"
parameterType 入参类型(实体 / Map) parameterType="User"
flushCache 刷新二级缓存(默认 true flushCache="true"

其中的基础用法,全字段更新

1
2
3
4
5
<update id="updateUser" parameterType="User">
UPDATE users
SET name = #{name}, email = #{email}, age = #{age}
WHERE id = #{id}
</update>

但是这样更新的话,若入参实体的 emailnull,会把数据库的 email 覆盖为 null

所以说,一般情况下,使用<update> 的动态部分字段更新

1
2
3
4
5
6
7
8
9
<update id="updateUserSelective" parameterType="User">
UPDATE users
<set>
<if test="name != null and name != ''">name = #{name},</if>
<if test="email != null and email != ''">email = #{email},</if>
<if test="age != null">age = #{age},</if>
</set>
WHERE id = #{id}
</update>
  • <set> 标签自动剔除最后一个字段后的逗号,避免 SQL 语法错误;
  • <if> 标签判断属性非空,仅更新有值的字段,解决全字段更新的 null 覆盖问题。

也能批量更新,也是使用<foreach>标签

1
2
3
4
5
6
7
8
9
10
<update id="batchUpdateUser" parameterType="java.util.List">
<foreach collection="list" item="user" separator=";">
UPDATE users
<set>
<if test="user.name != null">name = #{user.name},</if>
<if test="user.age != null">age = #{user.age},</if>
</set>
WHERE id = #{user.id}
</foreach>
</update>

<select>:查询标签

单条查询

我说,大哥来了

执行 SELECT 语句,是 MyBatis 中最灵活、功能最丰富的标签,单条 / 批量 / 分页 / 关联,都使用的这个

属性比较多,如下

属性名 作用 示例 是否必填
id 绑定 Mapper 接口查询方法名 id="findUserById"
parameterType 入参类型 parameterType="int"
resultType 结果类型(全类名 / 别名,简单查询用) resultType="User" 二选一
resultMap 结果映射集(复杂查询 / 多表关联用) resultMap="UserWithOrdersResultMap" 二选一
fetchSize 每次从数据库获取的行数(优化查询性能) fetchSize="100"
timeout 执行超时时间 timeout="10"
useCache 是否使用二级缓存(默认 true useCache="false"

单条查询查询出单条数据,没什么别的特殊的,大家都了解了,例如对于User findUserById(Integer id);的一个 Mapper 接口,它的 XML 如下

1
2
3
<select id="findUserById" parameterType="int" resultType="User">
SELECT id, name, email, age FROM users WHERE id = #{id}
</select>
批量查询

按条件批量查询,没什么特殊的,该怎么查就怎么查,只不过 Mapper 那边的接口要使用容器对应上,例如对于List<User> findUserByAge(Integer age);的一个 Mapper 接口,它的 XML 如下

1
2
3
<select id="findUserByAge" parameterType="int" resultType="User">
SELECT id, name, email, age FROM users WHERE age > #{age} ORDER BY id
</select>

<foreach> 的核心属性

属性名 作用 示例
collection 入参集合的类型(List 填 list,数组填 array,Map 填 key 名) collection="list"
item 遍历的单个元素别名 item="id"
open 拼接内容的开头字符 open="("
separator 元素之间的分隔符 separator=","
close 拼接内容的结尾字符 close=")"
index 遍历索引(List 是下标,Map 是 key) index="i"
  • 但是对于动态查询中 IN 条件、批量 OR 条件,得多说一句

    • IN 条件查询多个用户

      对于下面的 Mapper 中的接口方法

      1
      List<User> findUserByIds(List<Integer> ids);

      可以这样利用<foreach>

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <select id="findUserByIds" parameterType="java.util.List" resultType="User">
      SELECT id, name, email, age FROM users
      <where>
      id IN
      <foreach collection="list" item="id" open="(" separator="," close=")">
      #{id}
      </foreach>
      </where>
      </select>
    • 批量 OR 条件

      对于下面的 Mapper 中的接口方法

      1
      List<User> findUserByMultiCondition(List<User> userList);

      可以这样利用<foreach>

      1
      2
      3
      4
      5
      6
      7
      8
      <select id="findUserByMultiCondition" parameterType="java.util.List" resultType="User">
      SELECT id, name, email, age FROM users
      <where>
      <foreach collection="list" item="user" separator="OR">
      (name = #{user.name} AND age = #{user.age})
      </foreach>
      </where>
      </select>
动态条件查询

但是,我们通常使用动态条件查询,它更灵活

1
2
3
4
5
6
7
8
9
<select id="findUserByCondition" parameterType="User" resultType="User">
SELECT id, name, email, age FROM users
<where>
<if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if>
<if test="age != null">AND age = #{age}</if>
<if test="email != null">AND email = #{email}</if>
</where>
ORDER BY id
</select>
  • <where> 标签类似 WHERE 关键字,只不过,具体的逻辑内容如下

    • 若标签内有有效内容,自动添加 WHERE 关键字;
    • 然后会自动剔除内容开头的 AND/OR
    • 若标签内无有效内容,不生成 WHERE 子句,避免 SELECT * FROM users WHERE 语法错误
  • <trim> 自定义拼接规则

    • <where> 无法满足复杂需求时,用 <trim> 更灵活

      1
      2
      3
      4
      5
      6
      7
      8
      9
      <select id="findUserByCondition" parameterType="User" resultType="User">
      SELECT id, name, email, age FROM users
      <trim prefix="WHERE" prefixOverrides="AND | OR">
      <if test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</if>
      <if test="age != null">AND age = #{age}</if>
      <if test="email != null">AND email = #{email}</if>
      </trim>
      ORDER BY id
      </select>
      <trim> 属性 作用
      prefix 给拼接后的内容加前缀(如 WHERE
      prefixOverrides 剔除内容开头的指定字符串
      suffix 给拼接后的内容加后缀(如 ORDER BY id
      suffixOverrides 剔除内容结尾的指定字符串(如逗号)
  • 支持 <choose>/<when>/<otherwise> 实现多条件分支:

    • <choose> 是 排他性条件,满足一个就跳过剩余,一般是多条件互斥,而非简单的多条件叠加:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      <select id="findUserByPriorityCondition" parameterType="User" resultType="User">
      SELECT id, name, email, age FROM users
      <where>
      <choose>
      <!-- 优先级1:按ID精准查 -->
      <when test="id != null">AND id = #{id}</when>
      <!-- 优先级2:按手机号精准查 -->
      <when test="phone != null and phone != ''">AND phone = #{phone}</when>
      <!-- 优先级3:按姓名模糊查 -->
      <when test="name != null and name != ''">AND name LIKE CONCAT('%', #{name}, '%')</when>
      <!-- 默认条件:只查未删除的 -->
      <otherwise>AND is_deleted = 0</otherwise>
      </choose>
      </where>
      </select>

      最多只执行一个 <when>,无匹配则执行 <otherwise>

    • <if> 是 叠加条件,满足即加,<choose> 是 分支条件,只选其一。

分页查询

物理分页查询也能用select实现,就是原生 SQL 语句,只不过要传入分页的数据

1
2
3
4
5
6
7
8
<select id="findUserByPage" resultType="User">
SELECT id, name, email, age FROM users
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
</where>
ORDER BY id
LIMIT #{pageSize} OFFSET #{pageNum}
</select>

对应的 Mapper 接口的如下

1
List<User> findUserByPage(@Param("name") String name, @Param("pageNum") Integer pageNum, @Param("pageSize") Integer pageSize);

分页参数通常需要安全校验,避免传入非法分页参数,如 pageNum=-1pageSize=10000导致性能问题,可在 XML 内用 <if> 校验

1
2
3
4
5
6
7
8
9
10
11
12
13
<select id="findUserByPage" resultType="User">
SELECT id, name, email, age FROM users
<where>
<if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
</where>
ORDER BY id
<if test="pageNum != null and pageNum >= 0 and pageSize != null and pageSize > 0 and pageSize <= 100">
LIMIT #{pageSize} OFFSET #{pageNum}
</if>
<if test="(pageNum == null or pageNum < 0) or (pageSize == null or pageSize <= 0 or pageSize > 100)">
LIMIT 10 OFFSET 0 <!-- 默认分页:第0页,每页10条 -->
</if>
</select>

但是生产中很少手写分页 SQL,常用 PageHelper 插件,其核心原理是:

  1. 拦截 <select> 标签的执行,在 SQL 前拼接分页语法;
  2. 自动查询总条数(无需手动写 COUNT(*));
  3. 封装分页结果(PageInfo)。
1
2
3
4
5
6
// Service 层
public PageInfo<User> findUserByPage(String name, Integer pageNum, Integer pageSize) {
PageHelper.startPage(pageNum, pageSize); // 开启分页
List<User> userList = userMapper.findUserByCondition(new User().setName(name));
return PageInfo.of(userList); // 封装结果
}

PageHelper 会修改 <select> 标签的 SQL,自动添加 LIMIT/OFFSET,无需修改 XML 就能获得分页的效果

多表关联查询

多表关联查询上面说了,不说了

最后是使用 XML

最后,要配置 MyBatis 的 XML 文件路径

必须告诉MyBatis,你的 XML 映射文件放在哪里。在 application.yml 中添加:

1
2
mybatis:
mapper-locations: classpath:mapper/**Mapper.xml

这个配置是什么意思呢?

  • classpath: 表示从编译后的classes目录下找(也就是 resources 目录下的文件编译后会放到classes里)。你可以把它理解为 项目的根目录。
  • mapper/**Mapper.xml 是一个路径通配符:
    • mapper/是指在mapper文件夹下找,那么你的 mapper 的 xml 就应该放到 resources 目录下 mapper 文件夹里
    • **表示任意多级目录(包括子文件夹)这里面使用一个* 或者两个 * 都是可以的。
    • *Mapper.xml 表示文件名以 Mapper.xml 结尾。

因为 MyBatis 启动时,找到了你的 Mapper 接口后,需要去加载对应的 XML 文件,解析里面的 SQL 语句。如果不告诉它路径,它就会像无头苍蝇一样找不到文件,你调用 Mapper 接口时就会报错,说找不到对应的 SQL。所以这一步是必须的!

如何进行参数绑定与类型处理

首先,Mapper 接口传递过来的参数,Mapper XML 中对应的 SQL 是要进行接收和处理的,那么这个参数绑定如何进行,其中,涉及到类型问题又该如何正确处理

#{}${}

它们是做参数绑定的,也就是 Mapper 接口方法传入的参数,就是通过这两个语法填到 XML 配置文件中的 SQL 的

两者都是进行的是单参数绑定

语法 原理 优点 缺点 适用场景
#{} 预编译,参数占位符 防 SQL 注入,类型安全 无法动态拼接表名 / 列名 普通参数(值)
${} 字符串直接替换 支持动态表名 / 列名 有 SQL 注入风险 动态表名、列名(需手动过滤)

然后来详细看看这两者

  • #{参数名} 会将接口方法传入的参数,以「JDBC 预编译占位符(?)」的形式绑定到 SQL 中,MyBatis 会自动处理参数类型转换,能够保证类型安全,而且还能防 SQL 注入,是日常开发的首选。

    1
    2
    3
    4
    // 单参数:删除用户
    int deleteUserById(Integer id);
    // 多参数:条件查询(@Param指定参数名)
    List<User> findUserByNameAndAge(@Param("name") String name, @Param("minAge") Integer minAge);
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- 单参数绑定:#{id} 对应接口的 Integer id -->
    <delete id="deleteUserById">
    DELETE FROM users WHERE id = #{id}
    </delete>

    <!-- 多参数绑定:#{name}/#{minAge} 对应@Param的名称 -->
    <select id="findUserByNameAndAge" resultMap="UserResultMap">
    SELECT * FROM users
    <where>
    <if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if>
    <if test="minAge != null">AND age >= #{minAge}</if>
    </where>
    </select>

    因为 #{} 是占位符,只能替换 值,不能替换 SQL 关键字

  • ${参数名} 会将接口方法传入的参数,以 字符串直接拼接 的形式替换到 SQL 中,不做任何转义,仅适用于 动态表名 / 列名 等无法用 #{} 的场景。

    比如你需要动态指定查询的表名

    1
    2
    // 动态表名查询
    List<User> findUserByTable(@Param("tableName") String tableName, @Param("id") Integer id);
    1
    2
    3
    4
    <select id="findUserByTable" resultMap="UserResultMap">
    <!-- ${tableName} 直接替换为传入的表名,#{id} 用预编译 -->
    SELECT * FROM ${tableName} WHERE id = #{id}
    </select>

    ${tableName} 是 字符串替换,不是占位符,而 #{id} 仍为预编译占位符。

    所以说,它有 SQL 注入风险,而且无类型转换,需手动保证类型正确。安全使用 ${} 的前提是必须对传入的参数做严格过滤

多个参数绑定

Mapper 方法有多个参数时,MyBatis 会将参数封装为 Map,默认键为 param1、param2... 或参数索引

所以说,对于下面的 Mapper 接口,你可以这样写 XML 去做多个参数的绑定

1
2
// 根据姓名和年龄查询用户
List<User> selectUserByNameAndAge(String name, Integer age);
1
2
3
4
<select id="selectUserByNameAndAge" resultType="com.example.entity.User">
SELECT id, name, age FROM user
WHERE name = #{param1} AND age = #{param2} <!-- 或 #{0}、#{1} -->
</select>

这样做虽然可以,但是基本上没有人会这么做

我们都使用 @Param 注解,指定参数名称,提高可读性。

所以说接口都这么写

1
2
// 注解指定参数名
List<User> selectUserByNameAndAge(@Param("name") String name, @Param("age") Integer age);

XML 这样做参数绑定

1
2
3
4
<select id="selectUserByNameAndAge" resultType="com.example.entity.User">
SELECT id, name, age FROM user
WHERE name = #{name} AND age = #{age} <!-- 直接使用注解指定的名称 -->
</select>

MyBatis 3.4+ 支持通过编译参数直接使用参数名,无需 @Param 注解。

  1. Maven 编译插件添加参数:

1
2
3
4
5
6
7
8
9
10
><plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg> <!-- 保留参数名称 -->
</compilerArgs>
</configuration>
></plugin>

  1. Mapper 接口无需 @Param

1
2
>// 直接使用参数名name/age
>List<User> selectUserByNameAndAge(String name, Integer age);

  1. XML 中直接使用参数名:

1
>WHERE name = #{name} AND age = #{age}

但是通常没有人会这样写,这只能算是一个特性,约定大于配置还是太有用了我说

对象参数绑定

参数是自定义 JavaBean,如 User、Order,直接通过 #{属性名} 绑定。这样 MyBatis 会自己自动解析对象的 getter 方法,就好像如 #{name} 对应 getName(),而且支持嵌套属性

对于一个这样的实体类

1
2
3
4
5
6
public class User {
private Integer id;
private String name;
private Integer age;
// 省略 getter/setter
}

Mapper 接口如下

1
2
// 添加用户
int insertUser(User user);

对应的 XML 就可以这样写

1
2
3
<insert id="insertUser">
INSERT INTO user (name, age) VALUES (#{name}, #{age}) <!-- 直接使用User的属性名 -->
</insert>

Map参数绑定

参数是 Map 时,通过 #{key} 绑定 Map 中的值,适合参数不固定的场景。

1
2
// 动态条件查询
List<User> selectUserByMap(Map<String, Object> params);

Mapper XML

1
2
3
4
5
6
<select id="selectUserByMap" resultType="com.example.entity.User">
SELECT id, name, age FROM user
WHERE 1=1
<if test="name != null">AND name = #{name}</if>
<if test="age != null">AND age = #{age}</if>
</select>

集合 / 数组参数绑定

例如,我们使用传递一个数组参数来批量删除,需结合 @Param 注解,配合 <foreach> 标签使用

1
2
// 批量删除用户
int deleteUserByIds(@Param("ids") Integer[] ids);
1
2
3
4
5
6
<delete id="deleteUserByIds">
DELETE FROM user WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</delete>

List 参数也一样

1
2
// 批量插入用户
int batchInsertUser(@Param("users") List<User> users);
1
2
3
4
5
6
<insert id="batchInsertUser">
INSERT INTO user (name, age) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>

MyBatis 类型处理

我们都知道 Java 类型和数据库类型是对不上的,所以说,MyBatis 要承担这个类型转换的职责

MyBatis 内置了大部分常用类型的处理器

Java 类型 数据库类型 对应 TypeHandler
String VARCHAR/CHAR StringTypeHandler
Integer/int INT IntegerTypeHandler
Date DATE/TIMESTAMP DateTypeHandler
Boolean/boolean BIT/TINYINT BooleanTypeHandler

在参数绑定的时候,MyBatis 根据 Java 类型自动选择对应的 TypeHandler,将 Java 对象转为 JDBC 参数。

在结果映射的时候也类似,根据数据库字段类型和 Java 实体属性类型,自动选择 TypeHandler 转换为 Java 对象。

而当内置处理器无法满足需求时,可自定义 TypeHandler。

就对于这样的一个枚举类型,MyBatis 是没法自动处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用户状态枚举
public enum UserStatus {
ACTIVE(1, "激活"),
INACTIVE(0, "未激活");

private int code;
private String desc;

// 构造器、getter
public static UserStatus getByCode(int code) {
for (UserStatus status : values()) {
if (status.getCode() == code) {
return status;
}
}
return null;
}
}

所以我们需要自己实现类型转换器,实现 TypeHandler 接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import java.sql.*;

// 自定义枚举类型处理器(将枚举转换为数据库int类型)
public class UserStatusTypeHandler extends BaseTypeHandler<UserStatus> {

// 参数绑定:Java枚举 → 数据库值
@Override
public void setNonNullParameter(PreparedStatement ps, int i, UserStatus parameter, JdbcType jdbcType) throws SQLException {
ps.setInt(i, parameter.getCode()); // 将枚举的code存入数据库
}

// 结果映射:数据库值 → Java枚举(单列)
@Override
public UserStatus getNullableResult(ResultSet rs, String columnName) throws SQLException {
int code = rs.getInt(columnName);
return UserStatus.getByCode(code);
}

// 结果映射:数据库值 → Java枚举(索引)
@Override
public UserStatus getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
int code = rs.getInt(columnIndex);
return UserStatus.getByCode(code);
}

// 结果映射:存储过程
@Override
public UserStatus getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
int code = cs.getInt(columnIndex);
return UserStatus.getByCode(code);
}
}

然后进行注册,注册自定义 TypeHandler,虽然 XML 也能注册,但是一般使用注解注册

1
2
3
4
5
6
7
8
9
10
11
// 在实体类属性上指定
public class User {
private Integer id;
private String name;
// 指定类型处理器
@Result(column = "status", typeHandler = UserStatusTypeHandler.class)
private UserStatus status;
}

// 或在Mapper XML中指定
<result column="status" property="status" typeHandler="com.example.handler.UserStatusTypeHandler"/>