1. Mybatis多表关系映射核心概念回顾
在Java EE开发中,Mybatis作为一款优秀的持久层框架,其多表关系映射能力是处理复杂业务场景的利器。上一章我们探讨了一对一和一对多的基础映射配置,本章将深入讲解更复杂的多对多关系映射、延迟加载策略以及实际开发中的性能优化技巧。
我曾在电商系统开发中遇到一个典型场景:需要同时处理商品SKU与属性值之间的多对多关系,以及订单与商品之间的一对多关系。这种复杂关联正是Mybatis多表映射大显身手的场景。通过合理的配置,我们成功将原本需要5-6次数据库查询的操作优化到2次内完成。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 多对多关系映射实现
2.1 基础多对多模型设计
多对多关系在实际业务中非常常见,比如用户-角色、学生-课程、商品-分类等。Mybatis处理多对多关系通常需要借助中间表实现。以学生选课系统为例:
java复制// 学生实体
public class Student {
private Integer id;
private String name;
private List<Course> courses; // 多对多关联
}
// 课程实体
public class Course {
private Integer id;
private String name;
private List<Student> students; // 反向关联
}
// 中间表实体(可选)
public class StudentCourse {
private Integer studentId;
private Integer courseId;
private Date selectTime; // 扩展字段
}
2.2 XML配置实现方式
在Mapper XML中配置多对多关系,需要同时处理主表和中间表的关联:
xml复制<resultMap id="studentMap" type="Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- 多对多集合映射 -->
<collection property="courses" ofType="Course">
<id property="id" column="course_id"/>
<result property="name" column="course_name"/>
</collection>
</resultMap>
<select id="findStudentWithCourses" resultMap="studentMap">
SELECT s.*, c.id as course_id, c.name as course_name
FROM student s
LEFT JOIN student_course sc ON s.id = sc.student_id
LEFT JOIN course c ON sc.course_id = c.id
WHERE s.id = #{id}
</select>
注意:多对多查询容易出现笛卡尔积问题,当关联表数据量大时需要考虑分步查询或使用DISTINCT关键字
2.3 注解方式实现
对于偏好注解开发的团队,可以使用@Many和@Results注解:
java复制@Select("SELECT * FROM student WHERE id = #{id}")
@Results({
@Result(id=true, property="id", column="id"),
@Result(property="name", column="name"),
@Result(property="courses", column="id",
many=@Many(select="com.example.mapper.CourseMapper.findByStudentId"))
})
Student findByIdWithCourses(Integer id);
对应的CourseMapper中需要定义:
java复制@Select("SELECT c.* FROM course c " +
"JOIN student_course sc ON c.id = sc.course_id " +
"WHERE sc.student_id = #{studentId}")
List<Course> findByStudentId(Integer studentId);
3. 延迟加载与性能优化
3.1 延迟加载配置
Mybatis的延迟加载能有效减少不必要的数据库查询。全局配置在mybatis-config.xml中:
xml复制<settings>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
也可以在特定关联上单独设置:
xml复制<collection property="courses" ofType="Course" fetchType="lazy">
<!-- 映射配置 -->
</collection>
3.2 批量查询优化
N+1查询问题是ORM常见性能瓶颈。Mybatis提供了解决方案:
- 全局配置批量加载:
xml复制<settings>
<setting name="defaultExecutorType" value="BATCH"/>
</settings>
- 使用
@Fetch注解指定批量大小:
java复制@Fetch(size = 10)
@Many(select="com.example.mapper.CourseMapper.findByStudentId")
List<Course> courses;
3.3 二级缓存配置
对于读多写少的数据,合理使用二级缓存能显著提升性能:
xml复制<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>
重要提示:缓存配置需要考虑数据一致性,更新操作多的表不建议开启二级缓存
4. 复杂查询与动态SQL
4.1 嵌套结果与嵌套查询
Mybatis提供两种处理复杂关联的方式:
- 嵌套结果(嵌套映射):
xml复制<resultMap id="blogMap" type="Blog">
<collection property="posts" ofType="Post" resultMap="postMap"/>
</resultMap>
<resultMap id="postMap" type="Post">
<collection property="comments" ofType="Comment" resultMap="commentMap"/>
</resultMap>
- 嵌套查询(分步查询):
xml复制<resultMap id="blogMap" type="Blog">
<collection property="posts" column="id"
select="com.example.mapper.PostMapper.findByBlogId"/>
</resultMap>
4.2 动态SQL处理多表条件
xml复制<select id="findStudents" resultMap="studentMap">
SELECT s.* FROM student s
<where>
<if test="name != null">
AND s.name LIKE #{name}
</if>
<if test="courseIds != null and courseIds.size() > 0">
AND EXISTS (
SELECT 1 FROM student_course sc
WHERE sc.student_id = s.id
AND sc.course_id IN
<foreach item="id" collection="courseIds" open="(" separator="," close=")">
#{id}
</foreach>
)
</if>
</where>
</select>
5. 实战问题与解决方案
5.1 分页查询优化
多表关联分页是个经典难题,Mybatis-plus提供了优雅解决方案:
java复制Page<Student> page = new Page<>(1, 10);
QueryWrapper<Student> wrapper = new QueryWrapper<>();
wrapper.inSql("id", "SELECT student_id FROM student_course WHERE course_id = 1");
IPage<Student> result = studentMapper.selectPage(page, wrapper);
对于原生Mybatis,可以使用内存分页或子查询方式:
xml复制<select id="findByPage" resultMap="studentMap">
SELECT * FROM student WHERE id IN (
SELECT student_id FROM (
SELECT student_id, ROW_NUMBER() OVER() as row_num
FROM student_course
WHERE course_id = #{courseId}
) t WHERE t.row_num BETWEEN #{start} AND #{end}
)
</select>
5.2 批量插入优化
处理多对多关系时,批量插入中间表数据是常见需求:
java复制@Insert("<script>" +
"INSERT INTO student_course (student_id, course_id) VALUES " +
"<foreach collection='list' item='item' separator=','>" +
"(#{item.studentId}, #{item.courseId})" +
"</foreach>" +
"</script>")
int batchInsert(@Param("list") List<StudentCourse> relations);
5.3 属性为null时的更新策略
使用Mybatis-plus时,可以通过注解控制更新行为:
java复制@TableField(updateStrategy = FieldStrategy.IGNORED)
private String remark;
或者在全局配置中设置:
yaml复制mybatis-plus:
global-config:
db-config:
update-strategy: not_null
6. 高级特性与扩展
6.1 自定义类型处理器
处理复杂的JSON类型字段:
java复制@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JsonTypeHandler extends BaseTypeHandler<List<String>> {
// 实现类型转换逻辑
}
在映射配置中使用:
xml复制<resultMap id="productMap" type="Product">
<result property="tags" column="tags" typeHandler="com.example.handler.JsonTypeHandler"/>
</resultMap>
6.2 插件开发示例
实现一个SQL执行时间监控插件:
java复制@Intercepts({
@Signature(type= Executor.class, method="update", args={MappedStatement.class,Object.class}),
@Signature(type= Executor.class, method="query", args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})
})
public class PerformanceInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long start = System.currentTimeMillis();
Object result = invocation.proceed();
long end = System.currentTimeMillis();
System.out.println("SQL执行耗时: " + (end - start) + "ms");
return result;
}
}
6.3 Mybatis与Spring集成
Spring Boot中的最佳配置实践:
yaml复制mybatis:
mapper-locations: classpath*:mapper/**/*.xml
type-aliases-package: com.example.model
configuration:
map-underscore-to-camel-case: true
default-fetch-size: 100
default-statement-timeout: 30
7. 最佳实践与避坑指南
-
XML与注解的选择策略:
- 简单查询使用注解更简洁
- 复杂动态SQL建议使用XML配置
- 混合使用时注意避免重复定义
-
N+1问题排查技巧:
- 开启Mybatis SQL日志:
logging.level.com.example.mapper=DEBUG - 使用
@Lang注解配合自定义语言驱动优化
- 开启Mybatis SQL日志:
-
事务管理要点:
- 多表操作务必添加事务注解
- 注意事务传播行为设置
- 批量操作考虑手动刷新会话
-
版本兼容性注意:
- Mybatis与Mybatis-plus版本匹配
- 驱动版本与数据库版本兼容
- Spring Boot starter版本对齐
-
监控与调优建议:
- 集成Druid监控SQL执行
- 定期分析慢查询日志
- 使用Explain分析复杂查询执行计划
在实际项目开发中,我曾遇到一个典型性能问题:用户角色权限查询在数据量达到10万级时响应时间超过5秒。通过将多表连接查询改为分步查询+缓存策略,最终将响应时间控制在200ms以内。关键优化点包括:
- 使用
@CacheNamespace注解开启二级缓存 - 将5表连接查询拆分为3次单表查询
- 对不变的基础数据启用本地缓存
- 使用Redis缓存用户权限树
