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();
}