前言
在 MyBatis 的传统开发模式中,XML映射文件是核心,但随着项目复杂度提升,大量 XML 文件会增加维护成本。MyBatis 提供的注解开发模式,可直接在 Mapper 接口方法上标注注解来编写 SQL,无需编写 XML 映射文件,极大简化了开发流程。本文将全面讲解 MyBatis 注解开发的核心用法,包括基础 CRUD、动态 SQL、关联查询,对比 XML 模式的差异,让你快速掌握注解开发的精髓。
一、环境准备
1. 核心依赖(pom.xml)
沿用之前的依赖,无需新增:
xml
XML
<!-- MyBatis核心依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
<scope>runtime</scope>
</dependency>
<!-- JUnit单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<!-- 日志依赖 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
2. 数据库准备
沿用 Day47 的mybatis_demo数据库(user、order、order_item 表),无需重新创建。
3. 核心配置文件(mybatis-config.xml)
修改映射器配置,从 XML 扫描改为包扫描(注解开发核心):
xml
XML
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 全局设置 -->
<settings>
<!-- 下划线转驼峰自动映射 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 开启日志打印 -->
<setting name="logImpl" value="LOG4J"/>
<!-- 开启延迟加载(关联查询用) -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!-- 环境配置 -->
<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/mybatis_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"/>
<property name="username" value="root"/>
<property name="password" value="root"/>
</dataSource>
</environment>
</environments>
<!-- 注解开发:包扫描Mapper接口(替代XML映射器) -->
<mappers>
<package name="com.example.mapper"/>
</mappers>
</configuration>
二、注解开发核心:基础 CRUD
MyBatis 提供了基础 CRUD 注解,完全替代 XML 中的<select>/<insert>/<update>/<delete>标签:
| 注解 | 作用 | 对应 XML 标签 |
|---|---|---|
@Select |
查询 SQL | <select> |
@Insert |
新增 SQL | <insert> |
@Update |
修改 SQL | <update> |
@Delete |
删除 SQL | <delete> |
@Results |
结果集映射 | <resultMap> |
@Result |
单个字段映射 | <result>/<id> |
1. 实体类准备(沿用之前的 User/Order/OrderItem)
java
运行
java
package com.example.entity;
import java.util.Date;
import java.util.List;
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String email;
private Date createTime;
private List<Order> orderList; // 一对多关联订单
// 无参构造、getter/setter、toString(省略)
}
2. 基础 CRUD 注解实现(UserMapper)
java
运行
java
package com.example.mapper;
import com.example.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
/**
* 用户Mapper(纯注解开发,无XML)
*/
public interface UserMapper {
/**
* 基础查询:根据ID查询用户
* @Results 替代XML中的resultMap,解决字段与属性映射
*/
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "username", property = "username"),
@Result(column = "password", property = "password"),
@Result(column = "age", property = "age"),
@Result(column = "email", property = "email"),
@Result(column = "create_time", property = "createTime") // 下划线转驼峰
})
User findById(Integer id);
/**
* 基础查询:查询所有用户
* 复用@Results:使用@ResultMap指定已定义的结果集映射
*/
@Select("SELECT * FROM user")
@ResultMap("com.example.mapper.UserMapper.findById-Result") // 格式:接口全类名.方法名-Result
List<User> findAll();
/**
* 基础新增:添加用户(返回自增ID)
* @Options(useGeneratedKeys = true):开启自增ID返回
*/
@Insert("INSERT INTO user (username, password, age, email) VALUES (#{username}, #{password}, #{age}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int addUser(User user);
/**
* 基础修改:更新用户
*/
@Update("UPDATE user SET username = #{username}, password = #{password}, age = #{age}, email = #{email} WHERE id = #{id}")
int updateUser(User user);
/**
* 基础删除:删除用户
*/
@Delete("DELETE FROM user WHERE id = #{id}")
int deleteUser(Integer id);
}
3. 基础 CRUD 测试
java
运行
java
package com.example.test;
import com.example.entity.User;
import com.example.mapper.UserMapper;
import com.example.util.MyBatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class BasicAnnotationTest {
@Test
public void testFindById() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findById(1);
System.out.println("根据ID查询:" + user);
}
}
@Test
public void testFindAll() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> userList = mapper.findAll();
System.out.println("查询所有用户:");
userList.forEach(System.out::println);
}
}
@Test
public void testAddUser() {
try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = new User();
user.setUsername("zhaoliu");
user.setPassword("666666");
user.setAge(32);
user.setEmail("zhaoliu@test.com");
int rows = mapper.addUser(user);
System.out.println("新增受影响行数:" + rows + ",自增ID:" + user.getId());
}
}
}
三、注解开发进阶:动态 SQL
XML 中的动态 SQL 标签(<if>/<where>/<foreach>),在注解中通过@ScriptingLanguage + OGNL 表达式实现,核心是使用<script>标签包裹动态 SQL 片段。
1. 动态 SQL 注解实现(UserMapper 扩展)
java
运行
java
/**
* 动态SQL:条件查询用户(if + where)
*/
@Select({
"<script>",
"SELECT * FROM user",
"<where>",
" <if test='username != null and username != \"\"'>",
" AND username LIKE CONCAT('%', #{username}, '%')",
" </if>",
" <if test='age != null'>",
" AND age > #{age}",
" </if>",
"</where>",
"</script>"
})
@ResultMap("com.example.mapper.UserMapper.findById-Result")
List<User> findByCondition(@Param("username") String username, @Param("age") Integer age);
/**
* 动态SQL:批量删除(foreach)
*/
@Delete({
"<script>",
"DELETE FROM user WHERE id IN",
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>",
" #{id}",
"</foreach>",
"</script>"
})
int batchDelete(@Param("ids") List<Integer> ids);
/**
* 动态SQL:动态更新(if + set)
*/
@Update({
"<script>",
"UPDATE user",
"<set>",
" <if test='username != null and username != \"\"'>username = #{username},</if>",
" <if test='password != null and password != \"\"'>password = #{password},</if>",
" <if test='age != null'>age = #{age},</if>",
" <if test='email != null and email != \"\"'>email = #{email},</if>",
"</set>",
"WHERE id = #{id}",
"</script>"
})
int updateUserDynamic(User user);
2. 动态 SQL 测试
java
运行
java
@Test
public void testFindByCondition() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 条件:用户名包含"admin",年龄大于20
List<User> userList = mapper.findByCondition("admin", 20);
System.out.println("动态条件查询结果:");
userList.forEach(System.out::println);
}
}
@Test
public void testBatchDelete() {
try (SqlSession session = MyBatisUtil.getSqlSession(true)) {
UserMapper mapper = session.getMapper(UserMapper.class);
List<Integer> ids = List.of(5, 6); // JDK9+可用List.of,低版本用new ArrayList<>()
int rows = mapper.batchDelete(ids);
System.out.println("批量删除受影响行数:" + rows);
}
}
四、注解开发高级:关联查询(一对一 / 一对多)
注解开发中,关联查询通过@One(一对一)和@Many(一对多)注解实现,替代 XML 中的<association>和<collection>。
1. 一对一关联查询(订单→用户)
OrderMapper 注解实现
java
运行
java
package com.example.mapper;
import com.example.entity.Order;
import com.example.entity.User;
import org.apache.ibatis.annotations.*;
import java.util.List;
public interface OrderMapper {
/**
* 一对一关联查询:订单→用户(嵌套结果)
*/
@Select("SELECT o.*, u.username, u.age, u.email FROM `order` o LEFT JOIN user u ON o.user_id = u.id WHERE o.id = #{id}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "order_no", property = "orderNo"),
@Result(column = "total_amount", property = "totalAmount"),
@Result(column = "create_time", property = "createTime"),
@Result(column = "user_id", property = "userId"),
// 一对一关联:封装User对象
@Result(property = "user", javaType = User.class,
columns = {
@ColumnResult(column = "user_id", property = "id"),
@ColumnResult(column = "username", property = "username"),
@ColumnResult(column = "age", property = "age"),
@ColumnResult(column = "email", property = "email")
})
})
Order findOrderWithUser(Integer id);
/**
* 一对一关联查询:订单→用户(分步查询+延迟加载)
*/
@Select("SELECT * FROM `order` WHERE id = #{id}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "order_no", property = "orderNo"),
@Result(column = "total_amount", property = "totalAmount"),
@Result(column = "user_id", property = "userId"),
// @One:一对一分步查询,select指定查询用户的方法
@Result(property = "user", column = "user_id",
one = @One(select = "com.example.mapper.UserMapper.findById", fetchType = FetchType.LAZY))
})
Order findOrderWithUserLazy(Integer id);
}
2. 一对多关联查询(用户→订单)
UserMapper 扩展一对多查询
java
运行
java
/**
* 一对多关联查询:用户→订单(分步查询+延迟加载)
*/
@Select("SELECT * FROM user WHERE id = #{id}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "username", property = "username"),
@Result(column = "password", property = "password"),
@Result(column = "age", property = "age"),
@Result(column = "email", property = "email"),
@Result(column = "create_time", property = "createTime"),
// @Many:一对多分步查询,select指定查询订单的方法
@Result(property = "orderList", column = "id",
many = @Many(select = "com.example.mapper.OrderMapper.findOrdersByUserId", fetchType = FetchType.LAZY))
})
User findUserWithOrders(Integer id);
OrderMapper 新增查询订单方法
java
运行
java
/**
* 根据用户ID查询订单(供一对多关联查询使用)
*/
@Select("SELECT * FROM `order` WHERE user_id = #{userId}")
@Results({
@Result(id = true, column = "id", property = "id"),
@Result(column = "order_no", property = "orderNo"),
@Result(column = "total_amount", property = "totalAmount"),
@Result(column = "user_id", property = "userId")
})
List<Order> findOrdersByUserId(Integer userId);
3. 关联查询测试
java
运行
java
@Test
public void testFindOrderWithUserLazy() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
OrderMapper mapper = session.getMapper(OrderMapper.class);
// 第一步:仅查询订单,未查询用户(延迟加载)
Order order = mapper.findOrderWithUserLazy(1);
System.out.println("订单编号:" + order.getOrderNo());
// 第二步:调用user属性时,触发延迟加载,执行查询用户的SQL
System.out.println("关联用户:" + order.getUser().getUsername());
}
}
@Test
public void testFindUserWithOrders() {
try (SqlSession session = MyBatisUtil.getSqlSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.findUserWithOrders(1);
System.out.println("用户信息:" + user.getUsername());
System.out.println("关联订单:");
user.getOrderList().forEach(order -> System.out.println(order.getOrderNo()));
}
}
五、注解开发 vs XML 开发:对比与选型
| 维度 | 注解开发 | XML 开发 |
|---|---|---|
| 开发效率 | 高(无需编写 XML,直接写在接口上) | 低(需维护大量 XML 文件) |
| 维护成本 | 简单 SQL 易维护,复杂 SQL 难维护 | 复杂 SQL 易维护(结构清晰) |
| 功能完整性 | 支持大部分功能,动态 SQL 写法繁琐 | 支持所有功能,动态 SQL 语法更友好 |
| 可读性 | 简单 SQL 可读性高,复杂 SQL 可读性差 | 所有 SQL 结构清晰,可读性高 |
| 适用场景 | 简单 CRUD、快速开发、小型项目 | 复杂 SQL、动态 SQL、大型项目 |
选型建议
- 优先用注解:简单 CRUD、小型项目、快速原型开发;
- 优先用 XML:复杂动态 SQL、关联查询多、大型项目(便于 SQL 统一管理和优化);
- 混合使用:核心 CRUD 用注解,复杂 SQL(如多表联查、动态 SQL)用 XML(MyBatis 支持注解 + XML 混合开发)。
六、注解开发常见问题与解决方案
1. 结果集映射复用问题
- 问题:多个方法需要复用相同的
@Results映射; - 解决方案:
- 使用
@ResultMap指定已定义的结果集(如@ResultMap("com.example.mapper.UserMapper.findById-Result")); - 将通用结果集映射抽离为常量,通过反射复用(进阶)。
- 使用
2. 动态 SQL 语法错误
- 问题:注解中动态 SQL 的
<script>标签内语法报错; - 解决方案:
- 确保 SQL 片段用
{}包裹,字符串用""转义; - 避免换行符导致的语法错误,可将动态 SQL 写在一行(或格式化工具辅助)。
- 确保 SQL 片段用
3. 关联查询延迟加载失效
- 问题:分步查询未触发延迟加载;
- 解决方案:
- 核心配置文件开启
lazyLoadingEnabled=true和aggressiveLazyLoading=false; @One/@Many中指定fetchType = FetchType.LAZY。
- 核心配置文件开启
总结
- 注解开发核心 :通过
@Select/@Insert/@Update/@Delete实现基础 CRUD,@Results/@Result实现结果集映射,替代 XML 核心标签; - 动态 SQL 注解 :通过
<script>标签包裹 XML 风格的动态 SQL 片段,实现条件查询、批量操作; - 关联查询注解 :
@One实现一对一关联,@Many实现一对多关联,配合延迟加载提升性能; - 选型原则:简单场景用注解,复杂场景用 XML,也可混合使用;
- 核心优势:注解开发简化了开发流程,减少了文件数量,适合快速开发和小型项目;XML 开发更适合复杂 SQL 场景,便于维护和优化。
掌握 MyBatis 注解开发,你可以根据项目需求灵活选择开发模式,既享受注解的便捷性,也能通过 XML 处理复杂场景,真正做到 "因地制宜"。