MyBatis:注解开发全攻略 - 从 XML 迁移到混合模式最佳实践

1. 引言

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

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

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

2. 注解开发核心语法

基础 CRUD 注解

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

java 复制代码
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 中 的功能。

java 复制代码
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。

java 复制代码
// 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();
    }
}
java 复制代码
// 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 中的 和 标签。

java 复制代码
一对一关联(用户 - 身份证)
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);
}
java 复制代码
一对多关联(用户 - 订单)
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 管理复杂查询,简单表用注解快速开发。

混合模式配置:

java 复制代码
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:

java 复制代码
<!-- 原 XML 配置(可删除) -->
<select id="selectById" resultType="User">
  SELECT id, username, email FROM user WHERE id = #{id}
</select>
java 复制代码
// 用注解替换
@Select("SELECT id, username, email FROM user WHERE id = #{id}")
User selectById(Long id);
  • 迁移结果映射:
java 复制代码
<!-- 原 XML 结果映射(可删除) -->
<resultMap id="userResultMap" type="User">
  <id column="id" property="userId"/>
  <result column="username" property="userName"/>
</resultMap>
java 复制代码
// 用注解定义结果映射
@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 映射文件:
java 复制代码
<!-- 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 接口中声明方法:
java 复制代码
public interface OrderMapper {
    // 接口方法与 XML 中 id 对应
    List<Order> selectComplex(OrderQuery query);

    // 其他简单方法仍使用注解
    @Select("SELECT * FROM `order` WHERE id = #{id}")
    Order selectById(Long id);
}

用户模块迁移完整实现

  • 实体类定义:
java 复制代码
public class User {
    private Long userId;
    private String userName;
    private String email;
    private LocalDateTime createTime;
    private List<Order> orders; // 一对多关联订单
    // getter/setter
}
  • Mapper 接口(混合模式):
java 复制代码
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 补充:
java 复制代码
<!-- 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 构建类:
java 复制代码
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 管理。

正确配置方式:

java 复制代码
// 方式 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 方法中复用
java 复制代码
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 封装,避免参数顺序错误
java 复制代码
// 不推荐:参数顺序容易混淆
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"。

解决方案:

java 复制代码
// 实体类实现序列化
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 注解指定参数名。

解决方案:

java 复制代码
// 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();
}
相关推荐
Craaaayon5 小时前
如何选择两种缓存更新策略(写缓存+异步写库;写数据库+异步更新缓存)
java·数据库·redis·后端·缓存·mybatis
Seven9717 小时前
MyBatis 常见面试题
java·mybatis
小马爱打代码1 天前
MyBatis:入门到实战 - 配置与CRUD
mybatis
老友@2 天前
一次由 PageHelper 分页污染引发的 Bug 排查实录
java·数据库·bug·mybatis·pagehelper·分页污染
YDS8292 天前
苍穹外卖 —— Spring Cache和购物车功能开发
java·spring boot·后端·spring·mybatis
凌波粒2 天前
MyBatis完整教程IDEA版(2)--ResultMap/注解/一对多/多对一/lombok/log4j
java·intellij-idea·mybatis
ANGLAL2 天前
17.MyBatis动态SQL语法整理
java·sql·mybatis
北城以北88883 天前
SSM--MyBatis框架之缓存
java·缓存·intellij-idea·mybatis
凌波粒3 天前
MyBatis完整教程IDEA版(3)--动态SQL/MyBatis缓存
sql·intellij-idea·mybatis