1. 引言

在 MyBatis 发展初期,XML 配置几乎是实现 SQL 映射的唯一方式。开发者需要在 XML 文件中编写大量的 、 标签和结果映射配置,这种方式虽然实现了 SQL 与 Java 代码的分离,但也带来了文件臃肿、开发效率低、重构困难等问题。

近年来,随着注解开发模式的普及,越来越多的团队开始转向 MyBatis 注解开发。注解开发将 SQL 直接嵌入接口方法,省去了 XML 文件的繁琐配置,显著提升了开发效率。尤其在微服务和敏捷开发场景中,注解开发的即时性和简洁性更具优势。

本文将系统讲解 MyBatis 注解开发的核心语法,深入对比注解与 XML 配置的优劣,提供清晰的选型指南,并通过实战案例演示如何从纯 XML 项目平滑迁移到注解与 XML 混合模式,帮助开发者在不同场景下做出最优技术选择。

2. 注解开发核心语法

基础 CRUD 注解
MyBatis 提供了 @Select、@Insert、@Update、@Delete 四个基础注解,分别对应 SQL 的查询、插入、更新和删除操作。这些注解直接标注在 Mapper 接口的方法上,注解值为对应的 SQL 语句。

public interface UserMapper {
// 查询:根据 ID 获取用户
@Select("SELECT id, username, email, create_time FROM user WHERE id = #{id}")
User selectById(Long id);
// 插入:新增用户
@Insert("INSERT INTO user(username, email, create_time) " +
"VALUES(#{username}, #{email}, #{createTime})")
@Options(useGeneratedKeys = true, keyProperty = "id") // 自动生成主键并回填到实体
int insert(User user);
// 更新:根据 ID 更新用户信息
@Update("UPDATE user SET username = #{username}, email = #{email} WHERE id = #{id}")
int updateById(User user);
// 删除:根据 ID 删除用户
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteById(Long id);
}

关键参数说明:

  • #{}:参数占位符,MyBatis 会自动处理参数类型转换和 SQL 注入防护
  • @Options:配置额外选项,如 useGeneratedKeys 开启自动生成主键,keyProperty 指定主键在实体中的属性名

结果映射注解
当数据库字段名与实体类属性名不一致时,需要通过结果映射进行匹配。MyBatis 提供了 @Result、@Results、@ResultMap 注解实现类似 XML 中 的功能。

public interface UserMapper {
// 定义结果映射
@Results(id = "userResultMap", value = {
@Result(column = "id", property = "userId", id = true), // id=true 表示为主键
@Result(column = "username", property = "userName"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "status", property = "status",
typeHandler = UserStatusTypeHandler.class) // 自定义类型处理器
})
@Select("SELECT id, username, create_time, status FROM user WHERE id = #{id}")
User selectById(Long id);
// 复用已定义的结果映射
@ResultMap("userResultMap")
@Select("SELECT id, username, create_time, status FROM user WHERE username LIKE #{username}")
List<User> selectByUsernameLike(String username);
  }

注解说明:

  • @Results:定义结果映射集合,id 属性用于标识该映射,方便其他方法复用;

  • @Result:单个字段映射,column 为数据库字段名,property 为实体类属性名;

  • @ResultMap:引用已定义的结果映射,避免重复配置。

动态 SQL 注解
对于包含条件判断、循环等逻辑的动态 SQL,MyBatis 提供了 @SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider 四个 Provider 注解,通过 SQL 构建类生成动态 SQL。

// SQL 构建类:封装动态 SQL 生成逻辑
public class UserSqlProvider {
// 动态查询用户列表
public String selectByCondition(UserQuery query) {
return new SQL() {{
SELECT("id, username, email, create_time");
FROM("user");
if (query.getUsername() != null) {
WHERE("username LIKE CONCAT('%', #{username}, '%')");
}
if (query.getStatus() != null) {
WHERE("status = #{status}");
}
if (query.getStartTime() != null) {
WHERE("create_time >= #{startTime}");
}
ORDER_BY("create_time DESC");
}}.toString();
}
}
// Mapper 接口:使用 Provider 注解关联 SQL 构建类
public interface UserMapper {
@SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")
@ResultMap("userResultMap")
List<User> selectByCondition(UserQuery query);
  }

Provider 注解参数:

  • type:指定 SQL 构建类的 Class 对象;

  • method:指定 SQL 构建类中生成 SQL 的方法名。

SQL 构建类最佳实践:

  • 方法参数需与 Mapper 接口方法参数一致;
  • 推荐使用 MyBatis 提供的 SQL 类构建 SQL,避免字符串拼接错误;
  • 将复杂动态 SQL 逻辑封装在 SQL 构建类中,保持 Mapper 接口简洁。

关联查询注解
MyBatis 注解提供 @One 和 @Many 注解实现关联查询,分别对应一对一和一对多关系,替代 XML 中的 和 标签。

一对一关联(用户 - 身份证)
public interface UserMapper {
@Results(id = "userWithIdCardMap", value = {
@Result(column = "id", property = "userId", id = true),
@Result(column = "username", property = "userName"),
// 一对一关联:通过用户 ID 查询身份证信息
@Result(column = "id", property = "idCard",
one = @One(select = "com.example.mapper.IdCardMapper.selectByUserId",
fetchType = FetchType.LAZY)) // 延迟加载
})
@Select("SELECT id, username FROM user WHERE id = #{id}")
User selectUserWithIdCard(Long id);
}
public interface IdCardMapper {
@Select("SELECT id, user_id, card_no FROM id_card WHERE user_id = #{userId}")
IdCard selectByUserId(Long userId);
}
一对多关联(用户 - 订单)
public interface UserMapper {
@Results(id = "userWithOrdersMap", value = {
@Result(column = "id", property = "userId", id = true),
@Result(column = "username", property = "userName"),
// 一对多关联:通过用户 ID 查询订单列表
@Result(column = "id", property = "orders",
many = @Many(select = "com.example.mapper.OrderMapper.selectByUserId",
fetchType = FetchType.EAGER)) // 立即加载
})
@Select("SELECT id, username FROM user WHERE id = #{id}")
User selectUserWithOrders(Long id);
}
public interface OrderMapper {
@Select("SELECT id, user_id, amount FROM `order` WHERE user_id = #{userId}")
List<Order> selectByUserId(Long userId);
  }

关联查询注解参数:

  • select:指定关联查询的 Mapper 方法全限定名;

  • fetchType:加载策略,FetchType.LAZY 为延迟加载(按需加载),FetchType.EAGER 为立即加载。

3. 注解 vs XML:终极选型指南

开发效率对比
注解开发优势:

  • 即时性:SQL 与接口方法直接关联,开发时无需在 Java 类和 XML 文件间切换;

  • 简洁性:省去 XML 标签的冗余配置,一行注解即可完成简单 SQL 映射;

  • 重构方便:字段或方法名变更时,IDE 可直接定位到注解中的引用位置。

XML开发优势:

  • 集中管理:所有 SQL 集中在 XML 文件中,便于批量查找和修改

  • 语法友好:XML 中支持 SQL 格式化和换行,复杂 SQL 可读性更高

  • 动态 SQL 直观:、 等标签比 Provider 类更直观易懂

维护成本分析
在这里插入图片描述

场景适配原则
推荐使用注解开发的场景:

  • 简单 CRUD 操作:如单表的查询、插入、更新、删除
  • 快速迭代项目:需求频繁变更,需要快速开发和部署
  • 微服务模块:服务粒度小,表结构简单,SQL 逻辑不复杂
  • 小型团队:沟通成本低,无需严格的 SQL 规范约束

推荐使用 XML 开发的场景:

  • 复杂动态 SQL:包含多层条件判断、批量操作、子查询等
  • DBA 参与优化:需要 DBA 独立修改 SQL 而不影响开发代码
  • 大型团队协作:多人维护同一模块,需要统一的 SQL 管理规范
  • 历史遗留系统:已有大量 XML 配置,迁移成本高于维护成本

混合模式实践
在实际项目中,纯注解或纯 XML 都不是最优解,推荐采用混合模式:核心表用 XML 管理复杂查询,简单表用注解快速开发。

混合模式配置:

mybatis:
mapper-locations: classpath:mapper/*.xml # 扫描 XML 映射文件
type-aliases-package: com.example.entity # 实体类别名包
configuration:
map-underscore-to-camel-case: true # 开启驼峰命名映射

混合模式分工示例:

  • 用户表(核心表):

    简单查询(根据 ID 查询、分页查询):使用注解开发;
    复杂查询(多条件筛选、关联统计):使用 XML 开发;
    结果映射:在 XML 中定义全局 ,注解方法通过 @ResultMap 引用。

  • 字典表(简单表):

所有操作(查询、新增、修改):全部使用注解开发,无需 XML 文件。

4. 实战迁移案例

纯 XML 项目迁移步骤:

  • 保留核心 XML:先保留包含复杂动态 SQL 和关联查询的 XML 文件,不急于迁移

  • 替换简单 CRUD:

<!-- 原 XML 配置(可删除) -->
  <select id="selectById" resultType="User">
    SELECT id, username, email FROM user WHERE id = #{id}
    </select>
// 用注解替换
@Select("SELECT id, username, email FROM user WHERE id = #{id}")
User selectById(Long id);
  • 迁移结果映射:
<!-- 原 XML 结果映射(可删除) -->
  <resultMap id="userResultMap" type="User">
    <id column="id" property="userId"/>
      <result column="username" property="userName"/>
        </resultMap>
// 用注解定义结果映射
@Results(id = "userResultMap", value = {
@Result(column = "id", property = "userId", id = true),
@Result(column = "username", property = "userName")
})
@Select("SELECT id, username FROM user WHERE id = #{id}")
User selectById(Long id);
  • 逐步淘汰 XML:随着项目迭代,逐步用注解 + Provider 类替换 XML 中的动态 SQL,最终只保留极少数复杂场景的 XML 配置。

注解项目的 XML 补充
当注解项目遇到复杂场景需要 XML 支持时,可按以下步骤引入:

  • 创建 XML 映射文件:
<!-- src/main/resources/mapper/OrderMapper.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.OrderMapper">
      <!-- 复杂动态 SQL 查询 -->
        <select id="selectComplex" resultType="Order">
          SELECT * FROM `order`
          <where>
            <if test="userId != null">AND user_id = #{userId}</if>
              <if test="statusList != null and statusList.size() > 0">
                AND status IN
                <foreach collection="statusList" item="status" open="(" separator="," close=")">
                  #{status}
                  </foreach>
                    </if>
                      <if test="startTime != null">AND create_time >= #{startTime}</if>
                        </where>
                          ORDER BY create_time DESC
                          </select>
                            </mapper>
  • 在 Mapper 接口中声明方法:
public interface OrderMapper {
// 接口方法与 XML 中 id 对应
List<Order> selectComplex(OrderQuery query);
  // 其他简单方法仍使用注解
  @Select("SELECT * FROM `order` WHERE id = #{id}")
  Order selectById(Long id);
  }

用户模块迁移完整实现

  • 实体类定义:
public class User {
private Long userId;
private String userName;
private String email;
private LocalDateTime createTime;
private List<Order> orders; // 一对多关联订单
  // getter/setter
  }
  • Mapper 接口(混合模式):
public interface UserMapper {
// 简单查询:注解实现
@Results(id = "userBaseMap", value = {
@Result(column = "id", property = "userId", id = true),
@Result(column = "username", property = "userName"),
@Result(column = "create_time", property = "createTime")
})
@Select("SELECT id, username, create_time FROM user WHERE id = #{id}")
User selectBaseInfo(Long id);
// 动态条件查询:Provider 注解实现
@SelectProvider(type = UserSqlProvider.class, method = "selectByPage")
@ResultMap("userBaseMap")
List<User> selectByPage(UserPageQuery query);
  // 关联查询:注解 + XML 结合(结果映射在 XML 中定义)
  @ResultMap("userWithOrdersMap") // 引用 XML 中的 resultMap
  @Select("SELECT id, username FROM user WHERE id = #{id}")
  User selectWithOrders(Long id);
  // 简单插入:注解实现
  @Insert("INSERT INTO user(username, email, create_time) VALUES(#{userName}, #{email}, #{createTime})")
  @Options(useGeneratedKeys = true, keyProperty = "userId")
  int insert(User user);
  }
  • 复杂查询 XML 补充:
<!-- UserMapper.xml -->
  <mapper namespace="com.example.mapper.UserMapper">
    <!-- 复杂关联查询结果映射 -->
      <resultMap id="userWithOrdersMap" type="User">
        <id column="id" property="userId"/>
          <result column="username" property="userName"/>
            <!-- 一对多关联订单 -->
              <collection property="orders" ofType="Order"
              select="com.example.mapper.OrderMapper.selectByUserId"
              column="id"/>
              </resultMap>
                <!-- 超复杂 SQL:XML 实现 -->
                  <select id="selectUserStatistics" resultType="UserStatisticsVO">
                    SELECT
                    u.id, u.username,
                    COUNT(o.id) AS order_count,
                    SUM(o.amount) AS total_amount
                    FROM user u
                    LEFT JOIN `order` o ON u.id = o.user_id
                    <where>
                      <if test="startTime != null">o.create_time >= #{startTime}</if>
                        <if test="endTime != null">o.create_time <= #{endTime}</if>
                          </where>
                            GROUP BY u.id
                            HAVING order_count > 0
                            ORDER BY total_amount DESC
                            </select>
                              </mapper>
  • SQL 构建类:
public class UserSqlProvider {
public String selectByPage(UserPageQuery query) {
return new SQL() {{
SELECT("id, username, email, create_time");
FROM("user");
if (query.getUserName() != null) {
WHERE("username LIKE CONCAT('%', #{userName}, '%')");
}
if (query.getEmail() != null) {
WHERE("email = #{email}");
}
if (query.getCreateTimeStart() != null) {
WHERE("create_time >= #{createTimeStart}");
}
ORDER_BY("create_time DESC LIMIT #{offset}, #{pageSize}");
}}.toString();
}
}

5. 高级特性与陷阱规避

注解扫描配置
MyBatis 注解开发需要正确配置 Mapper 扫描,否则会导致接口无法被 Spring 管理。

正确配置方式:

// 方式 1:启动类添加 @MapperScan(推荐)
@SpringBootApplication
@MapperScan("com.example.mapper") // 扫描指定包下的所有 Mapper 接口
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
// 方式 2:在 Mapper 接口添加 @Mapper(适合少量接口)
@Mapper
public interface UserMapper { ... }

扫描冲突规避:

  • 避免 @MapperScan 与 @Mapper 同时使用,可能导致重复扫描
  • 扫描包路径不宜过宽(如直接扫描 com.example),会降低启动速度
  • 多模块项目需指定所有模块的 Mapper 包:@MapperScan({“com.module1.mapper”, “com.module2.mapper”})

Provider 类最佳实践

  • SQL 逻辑复用:将重复的 SQL 片段抽取为方法,在多个 Provider 方法中复用
public class UserSqlProvider {
// 复用的查询字段
private String baseColumns = "id, username, email, create_time";
public String selectByCondition(UserQuery query) {
return new SQL() {{
SELECT(baseColumns); // 复用字段定义
FROM("user");
// ... 条件逻辑
}}.toString();
}
}
  • 参数传递技巧:当需要多个参数时,推荐使用实体类或 Map 封装,避免参数顺序错误
// 不推荐:参数顺序容易混淆
public String selectByMultiParams(Long userId, String status) { ... }
// 推荐:使用实体类封装参数
public String selectByMultiParams(UserQuery query) {
return new SQL() {{
SELECT("*");
FROM("user");
if (query.getUserId() != null) {
WHERE("id = #{userId}");
}
if (query.getStatus() != null) {
WHERE("status = #{status}");
}
}}.toString();
}

常见陷阱
1、注解缓存配置失效
问题:在注解方法上使用 @CacheNamespace 后,二级缓存不生效。

原因:@CacheNamespace 注解需要配合实体类实现 Serializable 接口,且方法上不能有 useCache=“false”。

解决方案:

// 实体类实现序列化
public class User implements Serializable { ... }
// 正确配置缓存注解
@CacheNamespace(implementation = RedisCache.class)
public interface UserMapper {
@Select("SELECT * FROM user WHERE id = #{id}")
User selectById(Long id);
}

2、关联查询循环引用
问题:使用 @One 或 @Many 进行双向关联查询时,出现无限递归(如 User 包含 Order,Order 又包含 User)。

解决方案:

  • 避免双向关联查询,只在一方配置关联;
  • 使用 @JsonIgnore 注解忽略 JSON 序列化时的循环引用;
  • 关联查询时只返回必要字段,不包含反向引用属性。

3、Provider 类方法参数丢失

问题:Provider 方法中无法获取到 Mapper 接口传递的参数。
原因:Provider 方法参数名与 Mapper 接口参数名不一致,或未使用 @Param 注解指定参数名。

解决方案:

// Mapper 接口:使用 @Param 指定参数名
@SelectProvider(type = UserSqlProvider.class, method = "selectByParams")
List<User> selectByParams(@Param("name") String username, @Param("status") Integer status);
  // Provider 类:参数名需与 @Param 一致
  public String selectByParams(@Param("name") String username, @Param("status") Integer status) {
  return new SQL() {{
  SELECT("*");
  FROM("user");
  if (username != null) {
  WHERE("username LIKE #{name}"); // 注意使用 @Param 指定的名称
  }
  if (status != null) {
  WHERE("status = #{status}");
  }
  }}.toString();
  }