1. MyBatis N+1 问题详解
1.1 什么是N+1问题
N+1问题 是指执行1次主查询获取N条主记录,然后对每条主记录再执行1次关联查询,总共执行 1 + N 次查询的性能问题。
1.2 示例场景
假设有:部门表(department)和员工表(employee),一个部门有多个员工。
产生N+1问题的代码如下:
xml
<!-- 1. 先查询所有部门 -->
<select id="selectAllDepartments" resultMap="DepartmentResultMap">
SELECT id, name FROM department
</select>
<!-- 2. 为每个部门查询员工 -->
<select id="selectEmployeesByDeptId" resultType="Employee">
SELECT id, name FROM employee WHERE dept_id = #{deptId}
</select>
<!-- 3. 结果映射中使用嵌套查询 -->
<resultMap id="DepartmentResultMap" type="Department">
<id property="id" column="id"/>
<result property="name" column="name"/>
<collection property="employees" column="id"
ofType="Employee" select="selectEmployeesByDeptId"/>
</resultMap>
执行过程:
java
List<Department> departments = departmentMapper.selectAllDepartments();
// 实际执行的SQL:
// 1. SELECT id, name FROM department; (假设返回3个部门)
// 2. SELECT id, name FROM employee WHERE dept_id = 1;
// 3. SELECT id, name FROM employee WHERE dept_id = 2;
// 4. SELECT id, name FROM employee WHERE dept_id = 3;
// 总共执行了 1 + 3 = 4 次查询
1.3 产生问题
- 性能低下:查询次数随数据量线性增长
- 数据库压力大:频繁建立数据库连接
- 响应时间长:网络往返次数多
接下来讲解解决1+N问题的方式
2. 使用JOIN 查询 + Collection 映射
2.1 实体类定义
java
// 部门实体
@Getter
@Setter
public class Department {
private Long id;
private String name;
private List<Employee> employees; // 一对多关系
}
// 员工实体
@Getter
@Setter
public class Employee {
private Long id;
private String name;
private String position;
private Long deptId;
}
2.2 XML配置
由于 JOIN 查询有些情况会产生重复的部门数据,MyBatis 会自动处理这种重复,但需要使用<id>
标签指定好主键:
xml
<!-- 使用 JOIN 查询一次性获取所有数据 -->
<resultMap id="DepartmentWithEmployeesMap" type="Department">
<id property="id" column="dept_id"/><!-- 重要配置 -->
<result property="name" column="dept_name"/>
<!-- 使用 collection 映射一对多关系 -->
<collection property="employees" ofType="Employee" javaType="java.util.ArrayList">
<id property="id" column="emp_id"/><!-- 重要配置 -->
<result property="name" column="emp_name"/>
<result property="position" column="position"/>
<result property="deptId" column="dept_id"/>
</collection>
</resultMap>
<select id="selectDepartmentWithEmployees" resultMap="DepartmentWithEmployeesMap">
SELECT
d.id as dept_id,
d.name as dept_name,
e.id as emp_id,
e.name as emp_name,
e.position,
e.dept_id
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
WHERE d.id = #{id}
</select>
<!-- 查询多个部门及其员工 -->
<select id="selectAllDepartmentsWithEmployees" resultMap="DepartmentWithEmployeesMap">
SELECT
d.id as dept_id,
d.name as dept_name,
e.id as emp_id,
e.name as emp_name,
e.position,
e.dept_id
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
ORDER BY d.id, e.id
</select>
2.3 使用实例
java
@Service
public class DepartmentService {
@Autowired
private DepartmentMapper departmentMapper;
// 一次性获取部门及其所有员工,避免N+1问题
public Department getDepartmentWithEmployees(Long deptId) {
return departmentMapper.selectDepartmentWithEmployees(deptId);
}
// 获取所有部门及其员工
public List<Department> getAllDepartmentsWithEmployees() {
return departmentMapper.selectAllDepartmentsWithEmployees();
}
// 业务方法:统计各部门员工数量
public Map<String, Integer> getEmployeeCountByDepartment() {
List<Department> departments = departmentMapper.selectAllDepartmentsWithEmployees();
return departments.stream()
.collect(Collectors.toMap(
Department::getName,
dept -> dept.getEmployees() != null ? dept.getEmployees().size() : 0
));
}
}
2.4 复杂场景(多层嵌套)
假设有一个多层嵌套的复杂场景,表关系如下:
java
// 公司实体
public class Company {
private Long id;
private String name;
private List<Department> departments; // 一对多:公司有多个部门
}
// 部门实体
public class Department {
private Long id;
private String name;
private Long companyId; // 所属公司ID
private List<Employee> employees; // 一对多:部门有多个员工
private List<Project> projects; // 一对多:部门有多个项目
}
// 员工实体
public class Employee {
private Long id;
private String name;
private String position; // 新增字段
private Long deptId; // 所属部门ID
private List<Skill> skills; // 多对多:员工有多个技能
}
// 项目实体
public class Project {
private Long id;
private String name;
private Long deptId; // 所属部门ID
private Date startDate; // 新增字段
private Date endDate; // 新增字段
}
// 技能实体
public class Skill {
private Long id;
private String name;
private String category; // 新增字段:技能分类
}
// 员工技能关联实体(多对多中间表)
public class EmployeeSkill {
private Long id;
private Long employeeId;
private Long skillId;
private Integer proficiency; // 熟练程度
}
对应的Mapper映射如下:
xml
<!-- 更新后的结果映射,包含所有字段 -->
<resultMap id="CompanyResultMap" type="Company">
<id property="id" column="company_id"/>
<result property="name" column="company_name"/>
<collection property="departments" ofType="Department" resultMap="DepartmentResultMap"/>
</resultMap>
<resultMap id="DepartmentResultMap" type="Department">
<id property="id" column="dept_id"/>
<result property="name" column="dept_name"/>
<result property="companyId" column="company_id"/>
<collection property="employees" ofType="Employee" resultMap="EmployeeResultMap"/>
<collection property="projects" ofType="Project" resultMap="ProjectResultMap"/>
</resultMap>
<resultMap id="EmployeeResultMap" type="Employee">
<id property="id" column="emp_id"/>
<result property="name" column="emp_name"/>
<result property="position" column="position"/>
<result property="deptId" column="dept_id"/>
<collection property="skills" ofType="Skill" resultMap="SkillResultMap"/>
</resultMap>
<resultMap id="ProjectResultMap" type="Project">
<id property="id" column="project_id"/>
<result property="name" column="project_name"/>
<result property="deptId" column="dept_id"/>
<result property="startDate" column="start_date"/>
<result property="endDate" column="end_date"/>
</resultMap>
<resultMap id="SkillResultMap" type="Skill">
<id property="id" column="skill_id"/>
<result property="name" column="skill_name"/>
<result property="category" column="category"/>
</resultMap>
<!-- 更新后的查询SQL,包含所有字段 -->
<select id="selectCompanyWithDetails" resultMap="CompanyResultMap">
SELECT
c.id as company_id,
c.name as company_name,
d.id as dept_id,
d.name as dept_name,
d.company_id,
e.id as emp_id,
e.name as emp_name,
e.position,
e.dept_id,
p.id as project_id,
p.name as project_name,
p.dept_id,
p.start_date,
p.end_date,
s.id as skill_id,
s.name as skill_name,
s.category
FROM company c
LEFT JOIN department d ON c.id = d.company_id
LEFT JOIN employee e ON d.id = e.dept_id
LEFT JOIN project p ON d.id = p.dept_id
LEFT JOIN employee_skill es ON e.id = es.employee_id
LEFT JOIN skill s ON es.skill_id = s.id
WHERE c.id = #{id}
</select>
3. 分次查询+Stream处理
还有一种方式是通过分次(次数为关联表的个数)查询关联表后,再使用Stream流组装数据,下面是通过分次查询+Stream处理查询公司详情信息的方法:
java
@Service
public class CompanyService {
public Company getCompanyWithDetails(Long companyId) {
// 1. 查询公司
Company company = companyMapper.selectById(companyId);
if (company == null) return null;
// 2. 查询部门
List<Department> departments = departmentMapper.selectByCompanyId(companyId);
// 3. 查询员工(批量查询避免N+1)
List<Long> deptIds = departments.stream()
.map(Department::getId)
.collect(Collectors.toList());
List<Employee> employees = employeeMapper.selectByDeptIds(deptIds);
// 4. 使用Stream组装数据
Map<Long, List<Employee>> employeeMap = employees.stream()
.collect(Collectors.groupingBy(Employee::getDeptId));
departments.forEach(dept ->
dept.setEmployees(employeeMap.getOrDefault(dept.getId(), new ArrayList<>()))
);
company.setDepartments(departments);
return company;
}
}
4. 性能对比
同样的业务下(查询公司详情信息)他们的性能对比表如下:
方面 | JOIN+Collection | 分次查询+Stream |
---|---|---|
数据库查询次数 | 1次 | 3次 |
网络开销 | 低 | 中等 |
数据库压力 | 单次复杂查询 | 多次简单查询 |
内存占用 | 可能有重复数据 | 数据更紧凑 |
响应时间 | 稳定但可能较长 | 可能更快(并行查询) |
总结:建议在管理后台数据展示 这样的小数据量场景使用Join+Collection 方案,在API接口大数据量 这样较大数据量使用分次查询+Stream方案。