💡 一、前言
在实际开发中,我们经常需要处理多个表之间的关联关系。例如:
- 一个用户可以发表多篇文章(一对多)
- 每篇文章属于一个用户(多对一)
- 一篇文章可以有多个标签,一个标签也可以属于多篇文章(多对多)
而 MyBatis 作为 Java 领域最流行的 ORM 框架之一,提供了强大的对象关系映射能力,能够帮助我们优雅地处理这些复杂的关系。
本文将带你从零开始,手把手实现 MyBatis 中常见的三种关系映射,并结合 Spring Boot 给出完整示例,助你轻松应对面试与实战!
🛠️ 二、环境准备
1. 添加依赖(pom.xml
)
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis Starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok 简化实体类代码 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. 数据库配置(application.yml
)
spring:
datasource:
url: jdbc:mysql://localhost:3306/myblog
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.demo.model
📚 三、业务场景与数据库设计
我们以三个常见业务模型为例:
表名 | 描述 |
---|---|
user(id, name) | 用户表 |
article(id,title,content,user_id) | 文章表 |
tag(id, name) | 标签表 |
article_tag(article_id, tag_id) | 中间表 |
对应关系:
-
User和
Article
:一对多 / 多对一 -
Article和
Tag
:多对多
🧪 四、一对多(One-to-Many)映射实战
1. 定义实体类
// User.java
@Data
public class User {
private Integer id;
private String name;
private List<Article> articles; // 一对多
}
// Article.java
@Data
public class Article {
private Integer id;
private String title;
private String content;
private Integer userId;
}
2. Mapper 接口
@Mapper
public interface UserMapper {
List<User> findAllWithArticles();
}
3. XML 映射文件(UserMapper.xml)
✅ 方式一:联表查询一次性获取所有数据(推荐用于小数据量)
<mapper namespace="com.example.demo.mapper.UserMapper">
<resultMap id="UserWithArticles" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="articles" ofType="Article">
<id column="article_id" property="id"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
</collection>
</resultMap>
<select id="findAllWithArticles" resultMap="UserWithArticles">
SELECT u.id, u.name,
a.id AS article_id, a.title, a.content
FROM user u
LEFT JOIN article a ON u.id = a.user_id
</select>
</mapper>
✅ 方式二:嵌套查询 + 延迟加载(适合大数据量)
<resultMap id="UserWithArticlesLazy" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="articles"
column="id"
fetchType="lazy"
select="findArticlesByUserId"/>
</resultMap>
<select id="findAllWithArticlesLazy" resultMap="UserWithArticlesLazy">
SELECT * FROM user
</select>
<select id="findArticlesByUserId" resultType="Article">
SELECT * FROM article WHERE user_id = #{id}
</select>
🔁 五、多对一(Many-to-One)映射实战
1. 修改 Article 实体类
// Article.java
@Data
public class Article {
private Integer id;
private String title;
private String content;
private Integer userId;
private User user; // 多对一
}
2. Mapper 接口
@Mapper
public interface ArticleMapper {
List<Article> findAllWithUser();
}
3. XML 映射文件(ArticleMapper.xml)
<mapper namespace="com.example.demo.mapper.ArticleMapper">
<resultMap id="ArticleWithUser" type="Article">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<result column="user_id" property="userId"/>
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</association>
</resultMap>
<select id="findAllWithUser" resultMap="ArticleWithUser">
SELECT a.id, a.title, a.content, a.user_id,
u.name AS user_name
FROM article a
LEFT JOIN user u ON a.user_id = u.id
</select>
</mapper>
🔁 六、多对多(Many-to-Many)映射实战
1. 修改 Article 实体类
// Article.java
private List<Tag> tags; // 多对多
2. Tag 实体类
// Tag.java
@Data
public class Tag {
private Integer id;
private String name;
}
3. Mapper 接口
@Mapper
public interface ArticleMapper {
List<Article> findAllWithTags();
}
4. XML 映射文件(ArticleMapper.xml)
<resultMap id="ArticleWithTags" type="Article">
<id column="id" property="id"/>
<result column="title" property="title"/>
<result column="content" property="content"/>
<collection property="tags" ofType="Tag">
<id column="tag_id" property="id"/>
<result column="tag_name" property="name"/>
</collection>
</resultMap>
<select id="findAllWithTags" resultMap="ArticleWithTags">
SELECT a.id, a.title, a.content,
t.id AS tag_id, t.name AS tag_name
FROM article a
LEFT JOIN article_tag at ON a.id = at.article_id
LEFT JOIN tag t ON t.id = at.tag_id
</select>
🧭 七、进阶技巧与最佳实践
✅ 延迟加载 vs 立即加载
类型 | 特点 | 使用场景 |
---|---|---|
立即加载 | 一次性加载全部数据 | 数据量小,实时性要求高 |
延迟加载 | 用到时才查 | 数据量大,节省内存资源 |
配置延迟加载:
mybatis:
configuration:
lazy-loading-enabled: true
aggressive-lazy-loading: false
✅ 使用注解方式实现关联关系
@Select("SELECT * FROM article WHERE user_id = #{userId}")
@Results({
@Result(property = "id", column = "id"),
@Result(property = "user", column = "user_id",
one = @One(select = "com.example.demo.mapper.UserMapper.findById"))
})
List<Article> findByUserId(Integer userId);
✅ 分页 + 关联数据查询优化
使用 PageHelper 分页时,如果直接查询带有关联数据的大结果集,可能会导致性能问题。建议:
- 先分页主表数据;
- 再根据主表 ID 批量查询关联数据。
PageHelper.startPage(pageNum, pageSize);
List<Article> articles = articleMapper.findAll(); // 查询主表
List<Integer> ids = articles.stream().map(Article::getId).toList();
List<Tag> tags = tagMapper.findByArticleIds(ids); // 批量查询关联数据
🔍 八、常见问题与解决方案
问题 | 描述 | 解决方案 |
---|---|---|
返回集合为空 | SQL字段未正确映射 | 检查 <collection> 和字段别名 |
延迟加载无效 | 未开启全局配置 | 设置 lazy-loading-enabled: true |
多条记录重复 | 联表查询导致笛卡尔积 | 使用 <resultMap> 控制映射逻辑 |
注解方式无法识别 | 方法返回类型不匹配 | 确保使用 @Results 正确绑定属性 |
📘 九、总结
关系类型 | 使用标签 | 适用场景 |
---|---|---|
一对一 | <association> | 单个对象引用 |
一对多 | <collection> | 映射集合属性 |
多对多 | <collection> | 通过中间表连接 |
嵌套查询 | <collection select="..."> | 支持延迟加载 |
掌握这些核心映射技巧后,你可以:
- 在后台管理系统中构建复杂的业务模型;
- 在电商系统中实现商品与分类、标签等关联;
- 在内容平台中高效管理文章、用户与评论的关系;
- 应对 MyBatis 相关高频面试题。
📝 十、结语
无论是做后台开发还是参与企业级项目,MyBatis 的关联映射能力 都是必须掌握的核心技能。希望这篇文章能够帮助你更好地理解和应用 MyBatis 的一对多、多对一和多对多映射。
如果你觉得有用,欢迎点赞、收藏、转发给更多需要的朋友!