1. 为什么需要关联映射?
在数据库设计中,表与表之间的关系是不可避免的。常见的关系包括:
- 一对一(如用户与身份证信息)
- 一对多(如部门与员工)
- 多对多(如学生与课程)
传统JDBC开发中,我们需要手动编写大量SQL来处理这些关系,不仅代码冗长,而且维护困难。MyBatis的关联映射正是为了解决这个问题而生。
提示:MyBatis的关联映射不是简单的SQL封装,而是通过对象关系映射(ORM)的思想,将数据库关系自然地映射到Java对象中。
2. MyBatis关联映射的四种实现方式
2.1 嵌套结果映射(ResultMap)
这是最常用的方式,通过在resultMap中定义association或collection标签来实现:
xml复制<resultMap id="blogResultMap" type="Blog">
<id property="id" column="blog_id"/>
<result property="title" column="blog_title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="author_username"/>
</association>
<collection property="posts" ofType="Post">
<id property="id" column="post_id"/>
<result property="subject" column="post_subject"/>
</collection>
</resultMap>
2.2 嵌套查询(Nested Select)
通过额外的select语句加载关联数据:
xml复制<resultMap id="blogResultMap" type="Blog">
<association property="author" column="author_id"
javaType="Author" select="selectAuthor"/>
</resultMap>
<select id="selectAuthor" resultType="Author">
SELECT * FROM author WHERE id = #{id}
</select>
2.3 自动映射(Auto-Mapping)
当列名和属性名遵循特定命名规则时,MyBatis可以自动完成映射:
sql复制-- 查询语句
SELECT
b.id as blog_id,
b.title as blog_title,
a.id as author_id,
a.username as author_username
FROM blog b LEFT JOIN author a ON b.author_id = a.id
2.4 注解方式(Annotation)
使用Java注解替代XML配置:
java复制public interface BlogMapper {
@Select("SELECT * FROM blog WHERE id = #{id}")
@Results({
@Result(property = "author", column = "author_id",
one = @One(select = "org.example.mapper.AuthorMapper.selectAuthor"))
})
Blog selectBlog(int id);
}
3. 一对一关联映射实战
3.1 基本配置
假设我们有一个博客系统,每篇博客(Blog)对应一个作者(Author):
java复制public class Blog {
private int id;
private String title;
private Author author; // 一对一关系
// getters & setters
}
public class Author {
private int id;
private String username;
// getters & setters
}
3.2 XML配置示例
xml复制<resultMap id="blogWithAuthorResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<association property="author" javaType="Author">
<id property="id" column="author_id"/>
<result property="username" column="username"/>
</association>
</resultMap>
<select id="selectBlogWithAuthor" resultMap="blogWithAuthorResultMap">
SELECT
b.id, b.title,
a.id as author_id, a.username
FROM blog b
LEFT JOIN author a ON b.author_id = a.id
WHERE b.id = #{id}
</select>
3.3 性能优化建议
-
延迟加载:对于不总是需要的关联数据,可以配置懒加载
xml复制<settings> <setting name="lazyLoadingEnabled" value="true"/> </settings> -
联合查询vs嵌套查询:
- 联合查询:适合关联数据总是需要的情况
- 嵌套查询:适合关联数据不总是需要的情况
-
缓存策略:合理配置二级缓存可以减少数据库访问
4. 一对多关联映射详解
4.1 典型场景
一个博客(Blog)有多条评论(Comment):
java复制public class Blog {
private int id;
private String title;
private List<Comment> comments; // 一对多关系
// getters & setters
}
public class Comment {
private int id;
private String content;
// getters & setters
}
4.2 XML配置
xml复制<resultMap id="blogWithCommentsResultMap" type="Blog">
<id property="id" column="id"/>
<result property="title" column="title"/>
<collection property="comments" ofType="Comment">
<id property="id" column="comment_id"/>
<result property="content" column="content"/>
</collection>
</resultMap>
<select id="selectBlogWithComments" resultMap="blogWithCommentsResultMap">
SELECT
b.id, b.title,
c.id as comment_id, c.content
FROM blog b
LEFT JOIN comment c ON b.id = c.blog_id
WHERE b.id = #{id}
</select>
4.3 常见问题解决
-
N+1查询问题:
- 现象:主查询返回N条记录,然后对每条记录执行关联查询
- 解决方案:使用联合查询替代嵌套查询,或者配置批量加载
-
分页问题:
- 现象:一对多关联时,LIMIT作用于联合查询结果,可能导致主记录数量不准确
- 解决方案:使用嵌套查询+分页,或者在内存中处理分页
-
重复数据问题:
- 现象:联合查询可能导致主记录重复
- 解决方案:使用
<id>标签正确标识主键
5. 多对多关联映射实现
5.1 中间表处理
多对多关系通常通过中间表实现。例如学生(Student)和课程(Course):
sql复制CREATE TABLE student (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE course (
id INT PRIMARY KEY,
name VARCHAR(100)
);
CREATE TABLE student_course (
student_id INT,
course_id INT,
PRIMARY KEY (student_id, course_id)
);
5.2 Java模型设计
java复制public class Student {
private int id;
private String name;
private List<Course> courses;
// getters & setters
}
public class Course {
private int id;
private String name;
// getters & setters
}
5.3 MyBatis配置
xml复制<resultMap id="studentWithCoursesResultMap" 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="selectStudentWithCourses" resultMap="studentWithCoursesResultMap">
SELECT
s.id, s.name,
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>
5.4 性能优化技巧
-
批量插入中间表记录:
xml复制<insert id="insertStudentCourses" parameterType="map"> INSERT INTO student_course (student_id, course_id) VALUES <foreach collection="courseIds" item="courseId" separator=","> (#{studentId}, #{courseId}) </foreach> </insert> -
使用二级缓存:对于不经常变动的课程数据,可以启用缓存
-
分页查询优化:对于大量数据,考虑先查询学生ID,再分批加载课程
6. 动态关联映射技巧
6.1 按需加载关联数据
有时我们不需要总是加载所有关联数据。MyBatis提供了几种解决方案:
-
延迟加载:
xml复制<settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings> -
动态SQL决定是否加载:
xml复制<select id="selectBlog" resultMap="blogResultMap"> SELECT * FROM blog WHERE id = #{id} </select> <select id="selectBlogWithAuthor" resultMap="blogWithAuthorResultMap"> SELECT b.*, a.* FROM blog b LEFT JOIN author a ON b.author_id = a.id WHERE b.id = #{id} </select>
6.2 多重关联策略
对于复杂的对象图,可以组合使用不同的关联策略:
xml复制<resultMap id="detailedBlogResultMap" type="Blog">
<!-- 直接加载作者 -->
<association property="author" resultMap="authorResultMap"/>
<!-- 延迟加载评论 -->
<collection property="comments" select="selectCommentsByBlogId" column="id"/>
<!-- 按需加载标签 -->
<collection property="tags" ofType="Tag"
column="id" select="selectTagsByBlogId"
fetchType="lazy"/>
</resultMap>
6.3 关联映射的性能监控
-
启用SQL日志:监控实际执行的SQL语句
xml复制<settings> <setting name="logImpl" value="STDOUT_LOGGING"/> </settings> -
使用MyBatis插件:开发自定义插件统计SQL执行时间
-
数据库监控工具:如Druid的SQL监控功能
7. 高级主题:自定义类型处理器
7.1 处理复杂关联关系
有时标准的关联映射不能满足需求,我们可以自定义TypeHandler:
java复制@MappedTypes({List.class})
@MappedJdbcTypes(JdbcType.VARCHAR)
public class StringToListTypeHandler extends BaseTypeHandler<List<String>> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
List<String> parameter, JdbcType jdbcType) {
ps.setString(i, String.join(",", parameter));
}
@Override
public List<String> getNullableResult(ResultSet rs, String columnName) {
String value = rs.getString(columnName);
return value == null ? null : Arrays.asList(value.split(","));
}
// 其他重载方法...
}
7.2 注册和使用
xml复制<typeHandlers>
<typeHandler handler="com.example.StringToListTypeHandler"/>
</typeHandlers>
<resultMap id="userResultMap" type="User">
<result property="roles" column="roles"
typeHandler="com.example.StringToListTypeHandler"/>
</resultMap>
7.3 处理JSON类型字段
对于现代数据库支持的JSON类型,可以创建专门的TypeHandler:
java复制public class JsonTypeHandler<T> extends BaseTypeHandler<T> {
private final Class<T> type;
private final ObjectMapper objectMapper = new ObjectMapper();
public JsonTypeHandler(Class<T> type) {
this.type = type;
}
@Override
public void setNonNullParameter(PreparedStatement ps, int i,
T parameter, JdbcType jdbcType) {
try {
ps.setString(i, objectMapper.writeValueAsString(parameter));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) {
try {
String json = rs.getString(columnName);
return json == null ? null : objectMapper.readValue(json, type);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// 其他重载方法...
}
8. MyBatis-Plus对关联映射的增强
8.1 @TableField注解处理关联
MyBatis-Plus提供了更简洁的方式来处理一些关联关系:
java复制public class User {
@TableId
private Long id;
private String name;
@TableField(exist = false)
private List<Role> roles;
}
8.2 条件构造器中的关联查询
虽然MyBatis-Plus不直接支持关联查询,但可以结合使用:
java复制// 先查询主表
List<User> users = userMapper.selectList(
new QueryWrapper<User>().eq("status", 1)
);
// 批量查询关联数据
List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
Map<Long, List<Role>> roleMap = roleMapper.selectByUserIds(userIds).stream()
.collect(Collectors.groupingBy(Role::getUserId));
// 手动组装
users.forEach(user -> user.setRoles(roleMap.get(user.getId())));
8.3 存在则更新,不存在则插入
MyBatis-Plus提供了方便的upsert操作:
java复制userMapper.insertOrUpdate(user);
对应的XML配置:
xml复制<insert id="insertOrUpdate">
INSERT INTO user (id, name)
VALUES (#{id}, #{name})
ON DUPLICATE KEY UPDATE
name = VALUES(name)
</insert>
9. 安全注意事项
9.1 防止SQL注入
-
永远不要使用${}拼接SQL:
xml复制<!-- 危险! --> <select id="selectByOrder" resultType="User"> SELECT * FROM user ORDER BY ${columnName} </select> <!-- 安全 --> <select id="selectByOrder" resultType="User"> SELECT * FROM user ORDER BY #{columnName} </select> -
使用安全的动态SQL:
xml复制<select id="selectUsers" resultType="User"> SELECT * FROM user <where> <if test="name != null"> AND name = #{name} </if> <if test="status != null"> AND status = #{status} </if> </where> </select>
9.2 权限控制
-
数据库层面:为MyBatis使用的数据库用户分配最小必要权限
-
应用层面:在Service层进行数据权限过滤
-
MyBatis插件:开发插件自动添加权限过滤条件
9.3 日志安全
-
敏感数据脱敏:配置日志过滤器
xml复制<settings> <setting name="logPrefix" value="MyBatis:"/> <setting name="logImpl" value="SLF4J"/> </settings> -
生产环境关闭DEBUG日志:避免打印完整SQL和参数
10. 性能调优实战
10.1 批量操作优化
-
批量插入:
java复制@Insert("<script>" + "INSERT INTO user (name, age) VALUES " + "<foreach collection='list' item='item' separator=','>" + "(#{item.name}, #{item.age})" + "</foreach>" + "</script>") void batchInsert(@Param("list") List<User> users); -
批量更新:
xml复制<update id="batchUpdate"> <foreach collection="list" item="item" separator=";"> UPDATE user SET name = #{item.name}, age = #{item.age} WHERE id = #{item.id} </foreach> </update>
10.2 二级缓存配置
-
启用二级缓存:
xml复制<cache/> -
自定义缓存实现:
xml复制<cache type="org.mybatis.caches.ehcache.EhcacheCache"/> -
缓存策略选择:
- LRU - 最近最少使用
- FIFO - 先进先出
- SOFT - 软引用
- WEAK - 弱引用
10.3 连接池配置
推荐使用HikariCP:
properties复制# application.properties
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10
spring.datasource.hikari.idle-timeout=30000
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1
11. 常见问题排查
11.1 映射失败问题
症状:查询返回了数据,但Java对象属性为null
排查步骤:
- 检查列名和属性名是否匹配
- 检查resultMap配置是否正确
- 查看MyBatis日志确认实际SQL执行结果
- 检查TypeHandler是否正确注册
11.2 懒加载失效问题
症状:配置了懒加载但关联数据还是立即加载了
排查步骤:
- 确认lazyLoadingEnabled设置为true
- 检查aggressiveLazyLoading是否为false
- 确保没有在toString()方法中访问懒加载属性
- 检查日志确认是否触发了额外查询
11.3 性能问题
症状:查询速度慢,特别是关联查询
解决方案:
- 添加适当的数据库索引
- 优化SQL语句,避免SELECT *
- 考虑使用嵌套查询替代联合查询
- 合理使用二级缓存
12. 最佳实践总结
-
简单关联优先使用自动映射:保持代码简洁
-
复杂关联使用显式resultMap:提高可读性和可维护性
-
大数据量关联考虑懒加载:避免不必要的数据传输
-
频繁访问的只读数据使用缓存:减少数据库压力
-
批量操作优于单条操作:提高性能
-
定期检查执行计划:确保SQL高效执行
-
保持MyBatis版本更新:获取性能改进和新特性
在实际项目中,我通常会先设计好数据库关系,然后创建对应的Java模型,最后根据具体场景选择合适的关联映射策略。对于简单的CRUD操作,MyBatis-Plus提供了很大便利;对于复杂查询,原生的MyBatis映射更加灵活强大。关键是要理解每种方式的适用场景和性能影响,根据实际情况做出合理选择。
