@Many
导学
本节学习目标:
- 掌握
@Many注解的基本用法,能够替代 XML 中的<collection>完成一对多集合映射 - 理解
@Many的select属性,学会通过嵌套查询实现关联集合加载 - 掌握
fetchType属性,能够根据业务需求选择立即加载或延迟加载 - 展示嵌套查询的完整代码,包括主查询 Mapper 和关联集合查询 Mapper
定义
@Many 是 MyBatis 提供的一对多集合映射注解,用在 @Result 注解的 many 属性中,用于声明某个属性对应的是一个集合对象。它对应 XML 映射文件中的 <collection> 元素。
痛点解决:在查询班级信息时,通常需要同时获取该班级下的所有学生列表。如果不使用关联映射,开发者需要手动执行两次查询并自行组装对象树。@Many 通过声明嵌套查询,让 MyBatis 自动完成关联集合的查询和赋值,实现对象树的完整加载。
注解方式 vs XML 方式对比
| 对比维度 | @Many 注解方式 | XML <collection> 方式 |
|---|---|---|
| 声明位置 | 写在 @Result 的 many 属性中 | 写在 <resultMap> 内部 |
| 嵌套查询指定 | 通过 select 属性写方法全限定名 | 通过 select 属性写方法全限定名 |
| 加载策略 | 通过 fetchType 指定 LAZY / EAGER | 通过 fetchType 指定 lazy / eager |
| 可读性 | 关联简单时紧凑 | 关联复杂时 XML 层级更清晰 |
适用场景建议:一对多关联且集合元素结构简单时优先使用
@Many;集合元素极复杂或需要多层嵌套时,建议评估 XML<collection>。
适用位置与核心属性
@Many 只能用在 @Result 注解的 many 属性中,不能单独标注在方法上。
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
select | String | 是 | 嵌套查询方法的全限定名(Mapper 接口全路径.方法名),用于加载关联集合 |
fetchType | FetchType | 否 | 加载策略。FetchType.EAGER 立即加载(默认),FetchType.LAZY 延迟加载 |
核心原理
MyBatis 解析 @Result 时,如果发现 many 属性不为空,会创建一个嵌套查询的 ResultMapping,并标记为集合类型。执行主查询后,MyBatis 提取关联列的值作为参数,调用 select 属性指定的嵌套查询方法获取关联对象列表,然后将列表赋值给主对象的对应属性。如果 fetchType = LAZY,MyBatis 会生成集合代理,在首次访问该属性时才触发嵌套查询。
完整示例
场景说明
乐途公司学生管理系统中,学生按专业分组。需要查询每个专业的学生列表,展示"专业-学生列表"的一对多关系。
操作前的数据库表结构及初始数据
CREATE TABLE student (
stu_id INT PRIMARY KEY AUTO_INCREMENT,
stu_name VARCHAR(20),
stu_age INT,
stu_major VARCHAR(20),
stu_score DECIMAL(5,2)
);
INSERT INTO student (stu_name, stu_age, stu_major, stu_score) VALUES
('大翔', 22, '计算机科学', 95.5),
('白歌', 21, '软件工程', 88.0),
('小崔', 20, '计算机科学', 92.0),
('黄俪', 21, '信息安全', 90.5),
('李眉', 22, '软件工程', 87.0);
当前数据状态:
| stu_id | stu_name | stu_age | stu_major | stu_score |
|---|---|---|---|---|
| 1 | 大翔 | 22 | 计算机科学 | 95.50 |
| 2 | 白歌 | 21 | 软件工程 | 88.00 |
| 3 | 小崔 | 20 | 计算机科学 | 92.00 |
| 4 | 黄俪 | 21 | 信息安全 | 90.50 |
| 5 | 李眉 | 22 | 软件工程 | 87.00 |
完整的注解接口代码
实体类:Student(简化版,用于集合元素)
package com.flying.entity;
public class Student {
private Integer stuId;
private String stuName;
private Integer stuAge;
private Double stuScore;
public Integer getStuId() { return stuId; }
public void setStuId(Integer stuId) { this.stuId = stuId; }
public String getStuName() { return stuName; }
public void setStuName(String stuName) { this.stuName = stuName; }
public Integer getStuAge() { return stuAge; }
public void setStuAge(Integer stuAge) { this.stuAge = stuAge; }
public Double getStuScore() { return stuScore; }
public void setStuScore(Double stuScore) { this.stuScore = stuScore; }
@Override
public String toString() {
return "Student{stuId=" + stuId + ", stuName='" + stuName + "', stuAge=" + stuAge + ", stuScore=" + stuScore + "}";
}
}
实体类:Major(专业,包含学生列表)
package com.flying.entity;
import java.util.List;
public class Major {
private String majorName;
private List<Student> students;
public String getMajorName() { return majorName; }
public void setMajorName(String majorName) { this.majorName = majorName; }
public List<Student> getStudents() { return students; }
public void setStudents(List<Student> students) { this.students = students; }
@Override
public String toString() {
return "Major{majorName='" + majorName + "', students=" + students + "}";
}
}
Mapper 接口:StudentMapper(嵌套查询,供 @Many 引用)
package com.flying.mapper;
import com.flying.entity.Student;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface StudentMapper {
/**
* 根据专业名称查询该专业下的所有学生
* 该方法将被 MajorMapper 中的 @Many 引用
*/
@Select("SELECT stu_id, stu_name, stu_age, stu_score FROM student WHERE stu_major = #{majorName}")
@Results(value = {
@Result(column = "stu_id", property = "stuId", id = true),
@Result(column = "stu_name", property = "stuName"),
@Result(column = "stu_age", property = "stuAge"),
@Result(column = "stu_score", property = "stuScore")
})
List<Student> selectByMajorName(String majorName);
}
Mapper 接口:MajorMapper(主查询 + 集合映射)
package com.flying.mapper;
import com.flying.entity.Major;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Many;
import java.util.List;
public interface MajorMapper {
@Select("SELECT DISTINCT stu_major AS major_name FROM student")
@Results(id = "majorMap", value = {
@Result(column = "major_name", property = "majorName"),
@Result(column = "major_name", property = "students",
many = @Many(select = "com.flying.mapper.StudentMapper.selectByMajorName", fetchType = FetchType.EAGER))
})
List<Major> selectAllMajorsWithStudents();
}
测试调用代码
package com.flying.test;
import com.flying.entity.Major;
import com.flying.mapper.MajorMapper;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.InputStream;
import java.util.List;
public class ManyTest {
public static void main(String[] args) throws Exception {
InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession session = factory.openSession();
MajorMapper mapper = session.getMapper(MajorMapper.class);
System.out.println("=== 查询所有专业及其学生列表 ===");
List<Major> majors = mapper.selectAllMajorsWithStudents();
majors.forEach(System.out::println);
session.close();
}
}
实际执行结果
控制台 SQL 输出
=== 查询所有专业及其学生列表 ===
[main] DEBUG com.flying.mapper.MajorMapper.selectAllMajorsWithStudents - ==> Preparing: SELECT DISTINCT stu_major AS major_name FROM student
[main] DEBUG com.flying.mapper.MajorMapper.selectAllMajorsWithStudents - ==> Parameters:
[main] DEBUG com.flying.mapper.MajorMapper.selectAllMajorsWithStudents - <== Total: 3
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Preparing: SELECT stu_id, stu_name, stu_age, stu_score FROM student WHERE stu_major = ?
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Parameters: 计算机科学(String)
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - <== Total: 2
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Preparing: SELECT stu_id, stu_name, stu_age, stu_score FROM student WHERE stu_major = ?
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Parameters: 软件工程(String)
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - <== Total: 2
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Preparing: SELECT stu_id, stu_name, stu_age, stu_score FROM student WHERE stu_major = ?
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - ==> Parameters: 信息安全(String)
[main] DEBUG com.flying.mapper.StudentMapper.selectByMajorName - <== Total: 1
Major{majorName='计算机科学', students=[Student{stuId=1, stuName='大翔', stuAge=22, stuScore=95.5}, Student{stuId=3, stuName='小崔', stuAge=20, stuScore=92.0}]}
Major{majorName='软件工程', students=[Student{stuId=2, stuName='白歌', stuAge=21, stuScore=88.0}, Student{stuId=5, stuName='李眉', stuAge=22, stuScore=87.0}]}
Major{majorName='信息安全', students=[Student{stuId=4, stuName='黄俪', stuAge=21, stuScore=90.5}]}
查询结果集表格
| major_name | students 列表 |
|---|---|
| 计算机科学 | 大翔(95.5)、小崔(92.0) |
| 软件工程 | 白歌(88.0)、李眉(87.0) |
| 信息安全 | 黄俪(90.5) |
分析
@Many(select = "com.flying.mapper.StudentMapper.selectByMajorName")指定了嵌套查询方法的全限定名,MyBatis 通过major_name列的值作为参数调用该方法fetchType = EAGER表示立即加载,主查询执行后会立刻发起嵌套查询获取每个专业的学生列表- 嵌套查询会产生 N+1 问题:本例中查询 3 个专业,产生了 1 次主查询 + 3 次嵌套查询。在数据量大时,应考虑 JOIN 查询替代嵌套查询
@Many与@One的核心区别在于:@Many返回的是List集合,@One返回的是单个对象
易错场景 / 常见误区
| 误区 | 错误示例 | 正解 |
|---|---|---|
| select 属性写错方法名 | select = "StudentMapper.selectByMajorName" | 必须写全限定名 com.flying.mapper.StudentMapper.selectByMajorName |
| 关联列名与嵌套查询参数名不匹配 | column = "major_name" 但嵌套方法参数是 major | 确保嵌套查询方法的参数类型能接受关联列的值 |
| 集合属性未初始化 | Major 类中 students 未声明为 List | 集合属性类型必须是 List、Set 等集合接口或实现类 |
| 认为 EAGER 一定比 LAZY 好 | 所有关联都用 EAGER | 关联频繁访问时用 EAGER,偶尔访问时用 LAZY 减少不必要查询 |
面试考点
Q1:@Many 的 select 属性为什么要写全限定名?
MyBatis 需要通过全限定名在 Configuration 中定位到唯一的 MappedStatement。如果只写方法名,不同 Mapper 中可能存在同名方法,导致解析歧义。
Q2:@Many 嵌套查询会产生什么问题?如何优化?
会产生 N+1 查询问题:查询 N 条主记录,可能触发 N 次嵌套查询。优化方式:(1) 改用 JOIN 查询一次性获取所有数据,然后在内存中分组组装;(2) 开启延迟加载
fetchType = LAZY,只在需要时查询;(3) 使用缓存减少重复嵌套查询。
Q3:fetchType 的 LAZY 在注解方式下如何生效?
需要满足两个条件:(1)
fetchType = FetchType.LAZY;(2) 全局配置中aggressiveLazyLoading = false且lazyLoadingEnabled = true。MyBatis 会为集合属性生成 CGLIB 代理,在首次访问属性时触发嵌套查询。
Q4:@Many 和 @One 的核心区别是什么?
@Many表示关联属性是集合(一对多),对应 XML 的<collection>;@One表示关联属性是单一对象(一对一、多对一),对应 XML 的<association>。两者都通过select属性指定嵌套查询,但返回类型一个是List,一个是单个对象。
小结
@Many 让注解开发也能优雅地处理一对多关联映射。通过 select 指定嵌套查询、fetchType 控制加载策略,可以灵活应对不同业务场景。需要注意 N+1 问题,在性能敏感的场景下评估是否改用 JOIN 查询。
下一章引子
关联查询之外,方法多参数传递是日常开发的高频场景。@Param 注解负责为 Mapper 接口方法的参数命名,解决多参数时 MyBatis 无法识别参数名的问题。下一节将详细讲解 @Param 的用法。