MyBatis进行注解开发

如何理解注解开发

在 Spring Boot 中使用 MyBatis 注解开发替代 XML 配置,核心是通过 MyBatis 提供的注解直接在 Mapper 接口上编写 SQL,无需编写 Mapper 中 XML 文件的那些</select>等这种标签

使用注解实现基本 CURD

MyBatis 注解开发的核心是在 Mapper 接口 上通过注解编写 SQL,常用注解如下

注解 作用 示例
@Mapper 标记接口为 MyBatis Mapper 加在接口上(或用 @MapperScan 替代)
@Select 编写查询 SQL @Select("SELECT * FROM user WHERE id = #{id}")
@Insert 编写插入 SQL @Insert("INSERT INTO user(name, age) VALUES(#{name}, #{age})")
@Update 编写更新 SQL @Update("UPDATE user SET age = #{age} WHERE id = #{id}")
@Delete 编写删除 SQL @Delete("DELETE FROM user WHERE id = #{id}")
@Results 自定义结果映射(字段映射) 解决字段名与属性名不一致
@Result 单个字段的映射规则 配合 @Results 使用
@Param 给参数命名(多参数时必用) 方法参数前加 @Param("name") String name
@ResultType 指定返回值类型 当返回值类型无法自动推断时,显式指定
@Options 配置 SQL 执行选项 如自增主键、批量操作、超时时间等

那么,完整的 CURD 实现,如下,就改写我们之前的例子吧

  • 实体类如下

    1
    2
    3
    4
    5
    6
    7
    @Data
    public class User {
    private Long id; // 主键
    private String userName; // 用户名(对应数据库 user_name,开启驼峰自动映射)
    private Integer age; // 年龄
    private String email; // 邮箱
    }
  • UserMapper 可以这样被改写,目标是移除 UserMapper.xml,改用 MyBatis 注解编写 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
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    public interface UserMapper {
    // 增 - 替代insertUser的XML配置
    @Insert("INSERT INTO users (name, email, age) VALUES (#{name}, #{email}, #{age})")
    @Options(useGeneratedKeys = true, keyProperty = "id") // 自动回填主键
    int insertUser(User user);

    // 删 - 替代deleteUserById的XML配置
    @Delete("DELETE FROM users WHERE id = #{id}")
    int deleteUserById(Integer id);

    // 改 - 替代updateUser的XML配置(动态SQL)
    @Update({
    "<script>",
    "UPDATE users",
    "<set>",
    " <if test='name != null'>name = #{name},</if>",
    " <if test='email != null'>email = #{email},</if>",
    " <if test='age != null'>age = #{age},</if>",
    "</set>",
    "WHERE id = #{id}",
    "</script>"
    })
    int updateUser(User user);

    // 查单个 - 替代findUserById的XML配置
    @Select("SELECT id, name, email, age FROM users WHERE id = #{id}")
    User findUserById(Integer id);

    // 查全部 - 替代findAllUser的XML配置
    @Select("SELECT id, name, email, age FROM users ORDER BY id")
    List<User> findAllUser();

    // 多条件查询 - 替代findUserByNameAndAge的XML配置(动态SQL)
    @Select({
    "<script>",
    "SELECT id, name, email, age FROM users",
    "<where>",
    " <if test='name != null and name != \"\"'>",
    " AND name LIKE CONCAT('%', #{name}, '%')",
    " </if>",
    " <if test='minAge != null'>",
    " AND age >= #{minAge}",
    " </if>",
    "</where>",
    "ORDER BY id",
    "</script>"
    })
    List<User> findUserByNameAndAge(@Param("name") String name,
    @Param("minAge") Integer minAge);
    }
  • 然后调整 application.yaml 配置

    就是移除 MyBatis 对 XML 文件的扫描

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # MyBatis配置
    mybatis:
    # 移除XML扫描(注解开发不需要)
    # mapper-locations: classpath:mapper/*.xml
    # 实体类别名包
    type-aliases-package: hbnu.project.mybatisdemo.entity
    configuration:
    # 驼峰命名自动映射
    map-underscore-to-camel-case: true
    # 打印SQL日志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

这样,就能够使用注解进行开发了,可以看到就是注解然后内部写 SQL 语句

其中,插入的自动回填主键需要这样配置,基本就用到这两个配置项

1
@Options(useGeneratedKeys = true, keyProperty = "id") 
  • useGeneratedKeys = true:告诉 MyBatis 使用数据库的主键自增功能。
  • keyProperty = "id":指定将生成的主键值回填到实体类的哪个属性中。

那么,动态 SQL 可以看到也有一些区别

  • 对于动态 SQL 的支持,注解中可直接嵌入 XML 风格的动态 SQL 标签(if/where/foreach/set 等),解决复杂条件查询和更新
    • 注意:动态 SQL 标签需用 <script> ... </script> 包裹,才能被 MyBatis 识别
    • 注解开发虽便捷,但并非所有场景都适用,需根据业务选择,一般情况下,复杂 SQL 还是在 Mapper XML 中配置的多一些

也可以使用注解添加缓存,一级缓存默认开启,二级缓存需要在 Mapper 接口上添加 @CacheNamespace 注解来开启。

1
2
3
@Mapper
@CacheNamespace(implementation = PerpetualCache.class) // 开启二级缓存
public interface UserMapper { ... }

使用注解实现复杂关系映射开发

了解使用注解进行复杂关系开发

实现复杂关系映射之前我们可以在映射文件中通过配置<resultMap>来实现,在使用注解开发时我们需要借助@Result注解,@Result 注解等加上@One 注解,@Many 注解等注解实现询,替代 XML 中的 <association><collection> 标签。

复杂关系映射的注解如下

注解 核心作用 对应 XML 标签 适用场景
@Results 定义一组结果映射规则(可复用),是复杂映射的 “容器” <resultMap> 所有复杂映射的基础
@Result 单个字段 / 关联关系的映射规则,可配置普通字段或关联查询 <result>/<association>/<collection> 普通字段映射 + 关联关系映射
@One 嵌套查询(一对一),指定关联的单条结果查询方法 <association> 一对一关联(如用户 - 身份证)
@Many 嵌套查询(一对多),指定关联的多条结果查询方法 <collection> 一对多关联(如用户 - 订单)
@ResultMap 复用已定义的 @Results 规则,避免重复代码 resultMap="xxx" 多个查询复用同一套映射规则
@Param 关联查询中传递参数,解决多参数绑定问题 - 关联查询的参数传递

MyBatis 复杂映射的两种核心方式

  • 嵌套查询
    • 先查询主表数据,再根据主表的关联字段,如外键,调用另一个 Mapper 方法查询关联表数据;
    • 注解开发更友好
  • 嵌套结果
    • 通过一次多表联查(JOIN)获取所有数据,再通过注解拆分结果到关联对象,一次 SQL 查询就行
    • 复杂联查 SQL 写在注解中可读性差,一般是 XML 进行编写

一对一实际场景

用户(User)和身份证(IdCard)是一对一关系,一个用户只有一个身份证,身份证外键 user_id 关联用户主键 id

我们改写我们的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class User {

private Integer id;

private String name;

private String email;

private Integer age;

// 一对一关联的身份证对象
private IdCard idCard;

@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "', age=" + age + "}";
}
}

那么,身份证实体类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class IdCard {

private Long id;

// 身份证号
private String cardNo;

// 关联用户的外键
private Long userId;
}

那么,开始编写 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
public interface IdCardMapper {

/**
* 根据用户ID查询身份证(一对一基础查询)
*/
@Select("SELECT id, card_no AS cardNo, user_id AS userId FROM id_card WHERE user_id = #{userId}")
IdCard selectByUserId(Long userId);

/**
* 根据身份证ID查询身份证,并关联查询所属用户(一对一嵌套查询)
*/
@Select("SELECT id, card_no AS cardNo, user_id AS userId FROM id_card WHERE id = #{id}")
@Results({
// 基础字段映射
@Result(column = "id", property = "id"),
@Result(column = "card_no", property = "cardNo"),
@Result(column = "user_id", property = "userId"),
// 一对一关联查询,通过userId查询User,抛给selectByUserId方法进行查询,fetchType=EAGER立即加载
@Result(column = "user_id", property = "user",
one = @One(select = "hbnu.project.mybatisdemo.mapper.UsersMapper.selectById",
fetchType = FetchType.EAGER))
})
IdCard selectByIdWithUser(Long id);
}

修改 application.yaml,新增 MyBatis 注解 Mapper 扫描配置,替代 XML 的<mappers>标签

1
2
3
4
5
6
7
mybatis:
type-aliases-package: hbnu.project.mybatisdemo.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 扫描注解版Mapper接口
type-handlers-package: hbnu.project.mybatisdemo.mapper # 扫描Mapper接口

一对多实际场景

首先编写对应的实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class OrderInfo {

// 订单ID
private Long id;

// 订单编号
private String orderNo;

// 创建时间
private LocalDateTime createTime;

// 订单总金额
private BigDecimal totalAmount;

// 一对多:一个订单包含多个订单项
private List<OrderItem> orderItems;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Data
public class OrderItem {

// 订单项ID
private Long id;

// 关联订单ID
private Long orderId;

// 商品名称
private String productName;

// 商品单价
private BigDecimal productPrice;

// 购买数量
private Integer quantity;
}

然后编写两个对应的 Mapper,很明显,我们需要根据 id 查订单里对应的订单项

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
41
public interface OrderInfoMapper {

/**
* 根据ID查询订单,并关联查询所有订单项
*/
@Select("SELECT id, order_no, create_time, total_amount FROM order_info WHERE id = #{id}")
@Results({
// 映射订单自身字段
@Result(column = "id", property = "id"),
@Result(column = "order_no", property = "orderNo"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "total_amount", property = "totalAmount"),
// 一对多映射核心:通过order_id关联订单项
@Result(
// 订单实体中订单项列表的属性名
property = "orderItems",
// 关联字段是订单ID
column = "id",
many = @Many(select = "hbnu.project.mybatisdemo.mapper.OrderItemMapper.selectByOrderId", fetchType = FetchType.EAGER)
)
})
OrderInfo selectByIdWithItems(@Param("id") Long id);

/**
* 查询所有订单及关联订单项
*/
@Select("SELECT id, order_no, create_time, total_amount FROM order_info")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "order_no", property = "orderNo"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "total_amount", property = "totalAmount"),
@Result(
property = "orderItems",
column = "id",
many = @Many(select = "hbnu.project.mybatisdemo.mapper.OrderItemMapper.selectByOrderId")
)
})
List<OrderInfo> selectAllWithItems();
}

  • 那么,就使用@Results进行映射,把实体类中的字段正确通过@Result映射到数据库中的字段(数据库查询语句中的字段,因为可能涉及到别名)
  • 然后再使用@Result,把要查询的一对多中的多,丢给OrderItemMapper中的查询,去做一查多
1
2
3
4
5
6
7
8
9
public interface OrderItemMapper {

/**
* 根据订单ID查询所有订单项(供OrderInfoMapper的@Many调用)
*/
@Select("SELECT id, order_id, product_name, product_price, quantity FROM order_item WHERE order_id = #{orderId}")
List<OrderItem> selectByOrderId(@Param("orderId") Long orderId);
}

那么,Mapper 就写完了,服务层和控制器层就略了

测试一下,可以看到根据订单的 id 正确返回了多个订单项

image-20260323101442386
image-20260323101306305

多对多实际场景

这次使用课程和学生的对应关系,一门课程有多个学生选修,把上面没用过的@ResultMap给用一下

多对多关系需中间表关联课程和学生

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class Course {
// 课程ID
private Long id;
// 课程名称
private String courseName;
// 授课老师
private String teacher;
// 学分
private Integer credit;
// 多对多:一门课程被多个学生选择
private List<Student> students;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
public class Student {
// 学生ID
private Long id;
// 学生姓名
private String studentName;
// 年龄
private Integer age;
// 性别
private String gender;
// 多对多:一个学生选多门课程
private List<Course> courses;
}

Mapper 层核心是多对多,也是使用@Results定义结果映射的规则,然后使用 @ResultMap 在需要的地方复用映射规则,就例如我查出一个学生对应的课程用@Results定义好了,在下面查询多个学生的时候,直接使用 @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
31
public interface StudentMapper {

/**
* 定义结果映射规则
*/
@Results(id = "StudentWithCoursesResultMap", value = {
// 映射学生自身字段
@Result(column = "id", property = "id"),
@Result(column = "student_name", property = "studentName"),
@Result(column = "age", property = "age"),
@Result(column = "gender", property = "gender"),
// 通过学生ID查询关联的课程
@Result(
property = "courses", // 学生实体中课程列表的属性名
column = "id", // 关联字段(学生ID)
many = @Many( // 多对多本质是 一对多 的嵌套
select = "hbnu.project.mybatisdemo.mapper.CourseMapper.selectByStudentId",
fetchType = FetchType.EAGER // 立即加载,也可LAZY懒加载
)
)
})
@Select("SELECT id, student_name, age, gender FROM student WHERE id = #{id}")
Student selectByIdWithCourses(@Param("id") Long id);

/**
* 复用上面的ResultMap, @ResultMap引用ID
*/
@ResultMap("StudentWithCoursesResultMap") // 直接引用已定义的结果映射ID
@Select("SELECT id, student_name, age, gender FROM student")
List<Student> selectAllWithCourses();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
public interface CourseMapper {

/**
* 供StudentMapper的@Many调用:根据学生ID查询所选课程
*/
@Select("""
SELECT c.id, c.course_name, c.teacher, c.credit
FROM course c
JOIN student_course sc ON c.id = sc.course_id
WHERE sc.student_id = #{studentId}
""")
List<Course> selectByStudentId(@Param("studentId") Long studentId);
}

然后我们来测试一下

image-20260323110852616
image-20260323110839046