MyBatis中通过association实现多对一、collection实现一对多关联映射,使用resultMap解决字段名差异,结合JOIN完成多表查询。三表连接案例展示了套餐、明细与检查项的嵌套关联。
### 前言
所谓关联查询,就是在一个查询里同时从多个表中取数据,然后组合在一起展示。要实现关联,至少需要两个表。
先看一下数据库的建表语句,这里准备了`teacher`和`student`两张表,用外键关联:
```sql
DROP TABLE IF EXISTS student;
DROP TABLE IF EXISTS teacher;
CREATE TABLE teacher
(
tid int PRIMARY KEY auto_increment,
tname VARCHAR(30)
);
CREATE TABLE student
(
sid int PRIMARY KEY auto_increment,
sname VARCHAR(30),
age int,
tid int,
foreign key (tid) references teacher(tid)
);
```
对应的实体类也很直观。`Student`中除了自身字段,还增加了一个`Teacher`对象,方便后续关联查询时直接拿到老师信息:
```ja va
public class Student {
private Integer sid;
private String sname;
private Integer age;
private Teacher teacher;
// 自动生成 Getter、Setter、toString()、有参无参方法
}
```
```ja va
public class Teacher {
private Integer tid;
private String tname;
// 自动生成 Getter、Setter、toString()、有参无参方法
}
```
关于映射文件中几个关键标记,先记住三点:
1. 主键列用 `
` 标记
2. 普通列用 `` 标记
3. 关联查询的映射用 `` 或 `` 标记
下面开始正式介绍两种最常见的关联场景。
---
## 多对一关联 association
`` 标签用于描述一对一(或从“多”方角度的一对一)的关联关系。常见场景:多个学生属于同一个老师。
先在 `main/ja va` 中创建学生和老师的接口,以及对应的映射文件。映射文件的 `namespace` 必须是接口的完整限定名。
需求:查询所有学生,同时关联出对应的老师。所以学生接口中定义方法:
```ja va
public interface StudentMapper {
List queryAllStudents();
}
```
学生映射文件中,SQL 语句采用 SQL99 的 `JOIN` 写法:
```xml
select sid,sname,age,t.tid,tname
from student s join teacher t
on s.tid = t.tid
```
写好 SQL 后,一定要先在 Na vicat 里跑一遍,确保语法无误。

这里有一个细节:查询结果中 `t.tid` 和 `tname` 这两列,在 `Student` 类里并没有对应的普通属性,而是属于 `teacher` 对象的。所以需要手动配置 `resultMap`,把这两列映射到 `teacher` 属性上。

最终的映射文件如下:
```xml
select sid,sname,age,t.tid,tname
from student s join teacher t
on s.tid = t.tid
```
测试类也很好理解:获取 `StudentMapper`,直接调用 `queryAllStudents()`,然后循环打印结果即可。
```ja va
@Test
public void queryStudent() {
StudentMapper mapper = session.getMapper(StudentMapper.class);
List list = mapper.queryAllStudents();
for (Student student : list) {
System.out.println(student);
}
}
```

---
## 一对多关联 collection
`` 标签用于描述一对多的关联关系。比如一个老师对应多个学生。
需求:查询老师时,同时关联出该老师的所有学生。
首先修改 `Teacher` 的实体类,增加一个 `List` 属性——因为不确定有几个学生,用集合更合适:
```ja va
private List students;
// 生成Getter、Setter
```
老师接口中定义方法:
```ja va
List queryAllTeachers();
```
映射文件中,先写 SQL:
```xml
select t.tid,tname,sid,sname,age
from teacher t join student s
on t.tid = s.tid
```
同样,先在 Na vicat 里验证。

接下来配置结果映射。因为返回的是 `Teacher`,并且需要把学生集合映射到 `students` 属性上,所以用 ``:
```xml
```
别忘了在 `` 中引用这个 `resultMap`:
```xml
select t.tid,tname,sid,sname,age
from teacher t join student s
on t.tid = s.tid
```
测试类中,不仅打印老师信息,还能遍历它的学生列表:
```ja va
@Test
public void queryTeacher() {
TeacherMapper mapper = session.getMapper(TeacherMapper.class);
List list = mapper.queryAllTeachers();
for (Teacher teacher : list) {
System.out.println(teacher);
List sl = teacher.getStudents();
for (Student student : sl) {
System.out.println(student);
}
}
}
```

---
## resultMap元素 处理结果集映射
当数据库表的列名与 Ja va 对象的属性名不一致时,MyBatis 不会自动赋值。这时候就需要用到 `` 来手动映射。
`` 的子元素中,`` 标记主键列,`` 标记普通列。举个例子:
```xml
```
这样就能把数据库的 `cust_id` 列对应到 Ja va 对象的 `custId` 属性上。
---
## 三表连接查询案例
来看一个更贴近实际的项目场景:用户登录后选择体检套餐,男用户看到男士套餐,女用户看到女士套餐。

### 数据库中的对应关系

### PO 类
这里有三个表,所以需要三个 PO 类。
**套餐明细表(Setmealdetailed)**:包含自身字段,同时与检查项表是一对一关系,所以增加一个 `Checkitem` 对象。
```ja va
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Setmealdetailed {
private Long sdId;
private Long smId;
private Long ciId;
private Checkitem checkitem; // 一对一
}
```
**检查项表(Checkitem)**:纯字段,无关联。
```ja va
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Checkitem {
private Integer ciId;
private String ciName;
private String ciContent;
private String meaning;
private String remarks;
}
```
**套餐表(Setmeal)**:包含一个套餐的基本信息,同时一个套餐下有多个明细(一对多),所以用 `List`。
```ja va
import ja va.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Setmeal {
private Integer smId;
private String name;
private Integer type;
private Double price;
private List Setmealdetailed;
}
```
### Mapper 接口
```ja va
@Mapper
public interface SetmealMapper {
List querySetmeals(int type);
}
```
### Mapper 映射文件
这里涉及三张表联合查询,而且需要嵌套映射:套餐 → 套餐明细(collection)→ 检查项(association)。注意映射中的 `property` 和 `ofType` / `ja vaType` 要写对。
```xml
select *
from
setmealdetailed sd
join checkitem c
on c.ciId=sd.ciId
join setmeal sm
on sd.smId=sm.smId
where sm.type=#{type}
```
SQL 语句解释:通过三次 `join`,把套餐明细、检查项、套餐三张表关联起来,并用 `where` 条件根据套餐类型(type)筛选出对应的套餐。
```xml
select *
from
setmealdetailed sd
join checkitem c
on c.ciId=sd.ciId
join setmeal sm
on sd.smId=sm.smId
where sm.type=#{type}
```
### Service 接口与实现
```ja va
public interface SetmealService {
List querySetmeals(int type);
}
```
```ja va
@Service
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper mapper;
@Override
public List querySetmeals(int type) {
return mapper.querySetmeals(type);
}
}
```
### Controller 层
一个典型的 RESTful 接口,接收 `type` 参数并返回 JSON。
```ja va
@RestController
@RequestMapping("/setmeal")
public class SetmealController {
@Autowired
private SetmealService service;
@GetMapping
public List query(int type){
return service.querySetmeals(type);
}
}
```
### 测试结果(JSON)

### 前端代码
前端使用 Vue + axios 实现。挂载时从 sessionStorage 获取用户信息,根据用户性别(sex)调用对应的套餐接口。页面渲染时支持展开/折叠查看套餐明细。
```html
{{ this.text }}套餐
{{ setItem.name }}
检查类型
检查内容
检查意义
{{ setmealdetailed.checkitem.ciName }}
{{ setmealdetailed.checkitem.ciContent }}
{{ setmealdetailed.checkitem.meaning }}
```
对应的 CSS 样式,适配移动端布局:
```css
```
至此,从多对一到一对多,再到三表关联查询的完整案例就覆盖全了。实际项目中这类嵌套映射非常常见,把 `association` 和 `collection` 的用法理清,再复杂的关联查询也能轻松应对。