MyBatis 关联映射实战:深入解析多对一与一对多查询
在掌握基础的结果映射机制后,实际业务开发中往往面临复杂的数据结构,即数据分散在多个关联表中。MyBatis 通过 <association> 和 <collection> 标签,能够优雅地处理实体对象之间的多对一(一对一)、一对多及多对多关联关系。
数据库模型设计
为演示多表关联,我们基于 user(用户)与 account(账户)两张表构建示例环境。在此场景中,一个用户可以持有多个账户,而每个账户归属于唯一的一个用户。
1. 建表脚本
请在目标数据库中执行以下 SQL 语句创建数据结构:
USE mybatis_db;
-- 清理旧数据
DROP TABLE IF EXISTS `account`;
DROP TABLE IF EXISTS `user`;
-- 1. 构建用户主表
CREATE TABLE `user` (
`id` INT(11) NOT NULL AUTO_INCREMENT,
`username` VARCHAR(32) NOT NULL COMMENT '用户名',
`birthday` DATETIME DEFAULT NULL COMMENT '生日',
`sex` CHAR(1) DEFAULT NULL COMMENT '性别',
`address` VARCHAR(256) DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入测试用户数据
INSERT INTO `user`(`id`, `username`, `birthday`, `sex`, `address`)
VALUES
(1, '张三', '2023-01-01 10:00:00', '男', '北京'),
(2, '李四', '2023-02-02 11:00:00', '女', '上海'),
(3, '王五', '2023-03-03 12:00:00', '女', '深圳');
-- 2. 构建账户子表(含外键)
CREATE TABLE `account` (
`ID` INT(11) NOT NULL COMMENT '编号',
`UID` INT(11) DEFAULT NULL COMMENT '所属用户 ID',
`AMOUNT` DOUBLE DEFAULT NULL COMMENT '余额',
PRIMARY KEY (`ID`),
CONSTRAINT `fk_account_user` FOREIGN KEY (`UID`) REFERENCES `user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 插入测试账户数据
INSERT INTO `account`(`ID`, `UID`, `AMOUNT`)
VALUES
(1, 1, 5000.00),
(2, 1, 3000.00),
(3, 2, 8000.00);
数据录入完成后,表结构大致如下所示(示意):
2. 关联关系定义
- 多对一 (Many-to-One):
- 场景:从账户视角出发,查找其关联的用户。
- 实现:使用
<association>标签。 - 代码体现:在
Account类中包含User属性。 - 一对多 (One-to-Many):
- 场景:从用户视角出发,一次性获取该用户的多个账户。
- 实现:使用
<collection>标签。 - 代码体现:在
User类中包含List集合。
项目环境配置
创建一个标准的 Maven 项目,命名为 MyBatis_Relation_Demo,并引入必要的依赖库。
<project xmlns="http://maven.apache.org/POM/4.0.0" ...>
<modelVersion>4.0.0</modelVersion>
<groupId>com.mybatis.demo</groupId>
<artifactId>MyBatis_Relation_Demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!-- 数据库驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- MyBatis 核心包 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- 单元测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
场景一:多对一查询(Account → User)
需求:查询所有账户信息,并同步获取该账户关联的用户名称。
1. 实体类设计
修改 Account 对象,增加 User 成员变量以承接关联数据。
public class Account {
private Integer id;
private Integer uid;
private Double amount;
// 多对一关联:单个 User 对象
private User user;
// Getter & Setter 省略...
}
2. Mapper 接口与 XML 映射
在 AccountMapper.xml 中利用 resultMap 结合 <association> 完成映射。
<mapper namespace="com.mybatis.demo.mapper.AccountMapper">
<!-- 自定义结果映射:处理多对一 -->
<resultMap id="accUserMap" type="com.mybatis.demo.model.Account">
<!-- 账户自身字段映射 -->
<id property="id" column="id"/>
<result property="uid" column="uid"/>
<result property="amount" column="amount"/>
<!-- 关联用户对象 -->
<association property="user" javaType="com.mybatis.demo.model.User">
<result property="username" column="username"/>
<result property="address" column="address"/>
</association>
</resultMap>
<!-- 执行 SQL:隐式内连接 -->
<select id="findAllAccWithUser" resultMap="accUserMap">
SELECT a.id, a.uid, a.amount, u.username, u.address
FROM account a
JOIN user u ON a.uid = u.id
</select>
</mapper>
3. 测试验证
运行测试代码,控制台输出应包含嵌套的 User 对象信息:
@Test
public void testAccQuery() throws IOException {
SqlSession session = MyUtil.getSession();
AccountMapper mapper = session.getMapper(AccountMapper.class);
List<Account> list = mapper.findAllAccWithUser();
list.forEach(System.out::println);
session.close();
}
执行截图效果如下:
场景二:一对多查询(User → Accounts)
需求:查询所有用户,并列出该用户下所有的账户列表。
1. 实体类改造
此时需要在 User 类中添加集合属性:
public class User {
private Integer id;
// ... 其他字段
// 一对多关联:账户集合
private List<Account> accounts;
// Getters & Setters
}
2. Mapper XML 配置
关键在于使用 <collection> 标签来接收集合数据。注意处理列别名以避免主键冲突(当 User 和 Account 有相同 ID 时)。
<mapper namespace="com.mybatis.demo.mapper.UserMapper">
<resultMap id="usrAccMap" type="com.mybatis.demo.model.User">
<!-- 主表映射 -->
<id property="id" column="uid"/>
<result property="username" column="uname"/>
<!-- 集合映射 -->
<collection property="accounts" ofType="com.mybatis.demo.model.Account">
<id property="id" column="aid"/>
<result property="amount" column="amnt"/>
</collection>
</resultMap>
<select id="findAllUserWithAccs" resultMap="usrAccMap">
-- 左连接以确保无账户的用户也能查出
SELECT u.id as uid, u.username as uname,
a.id as aid, a.amount as amnt
FROM user u
LEFT JOIN account a ON u.id = a.uid
</select>
</mapper>
3. 测试与结果
调用 UserMapper 的查询方法,结果将打印出带有嵌套账户列表的用户信息。
@Test
public void testUserQuery() throws Exception {
SqlSession session = MyUtil.getSession();
UserMapper mapper = session.getMapper(UserMapper.class);
List<User> users = mapper.findAllUserWithAccs();
users.forEach(u -> System.out.println(u.getUsername() + " | " + u.getAccounts()));
session.close();
}
控制台输出示例显示列表数据已被正确封装:
核心差异总结
两种查询模式均依赖于 resultMap 的配置,主要区别在于关联属性的类型与对应的标签选择:
| 对比维度 | 多对一 (Many-to-One) | 一对多 (One-to-Many) |
|---|---|---|
| 核心标签 | <association> |
<collection> |
| 属性类型 | 单一对象 (Object) | 泛型集合 (List<Type>) |
| 指定类型方式 | javaType |
ofType |
| SQL 方向 | 通常查子表带父表 | 通常查父表带子表 |