六、Mybatis 多对一和一对多关系的处理
10、多对一处理
10.1、环境准备
多对一的关系在数据库中十分常见,例如:一个老师教很多学生,从老师的角度来是一对多的关系,而从学生的角度来看就是多对一的关系。我们重新创建一个项目,来准备下多对一场景需要的环境数据,这里就用学生和老师作为例子。
1、新建一张学生表和老师表,设置学生表中老师id为外键:
CREATE TABLE `teacher`(
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
PRIMARY KEY (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO teacher(`id`,`name`) VALUES (1, '秦老师')
CREATE TABLE `student`(
`id` INT(10) NOT NULL,
`name` VARCHAR(30) DEFAULT NULL,
`tid` INT(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fktid` (`tid`),
CONSTRAINT `fktid` FOREIGN KEY (`tid`) REFERENCES `teacher` (`id`)
)ENGINE=INNODB DEFAULT CHARSET=utf8
INSERT INTO student(`id`,`name`,`tid`) VALUES (1, '小红', 1)
INSERT INTO student(`id`,`name`,`tid`) VALUES (2, '小明', 1)
INSERT INTO student(`id`,`name`,`tid`) VALUES (3, '小方', 1)
INSERT INTO student(`id`,`name`,`tid`) VALUES (4, '小李', 1)
INSERT INTO student(`id`,`name`,`tid`) VALUES (5, '小王', 1)
2、整理一下项目结构:
-
将*Mapper.xml文件移动到resources下的com/luca/dao下,避免dao包下内容杂乱,并且resources下的com.luca.dao目录和java下的com.luca.dao目录再target中回合并,这样不影响编译后查找资源。
-
在pojo中创建Student和Teacher的实体类(项目结构参考上图)
pojo/Teacher.java
package com.luca.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Teacher { private int id; private String name; }
pojo/Student.java
package com.luca.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @NoArgsConstructor @AllArgsConstructor public class Student { private int id; private String name; private Teacher teacher; }
10.2、测试使用
现在我们想要从student表中查询出学生的数据,按照之前的方式:
-
StudentMapper
List<Student> getStudents();
-
StudentMapper.xml
<select id="getStudents" resultType="student"> select * from student </select>
-
测试MyTest
@Test public void TestGetTeacherById() { SqlSession sqlSession = MybatisUtils.getSqlSession(); StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); List<Student> students = mapper.getStudents(); for (Student student : students) { System.out.println(student); } sqlSession.close(); }
执行测试方法,我们看输出的结果:
虽然我们查出了所有的学生信息,但是其中teacher字段显示的是null,如果这里需要展示老师的姓名,该怎样处理呢?
10.3、按照结果嵌套处理(关联查询方式)
在进行多对一查询之前,我们需要确立两个概念。多个学生对应一个老师,那么从学生的角度看,老师是一个关联association
;相反的,从老师的角度看,老师拥有一个学生的集合collection
。
首先来看一种关联查询的方式,也是我比较好接受的方式:
StudentMapper中自然不用改变,仍是:
List<Student> getStudents();
在StudentMapper.xml中我们需要借助ResultMap来做一些操作了:
<select id="getStudents" resultMap="StudentWithTeacher">
select s.id sid, s.name sname, t.name tname
from student s, teacher t
where s.tid = t.id
</select>
<!-- 如果直接使用sql语句来查询,这不难实现,我们通过上面的语句就可以实现,我们把它写在select中 -->
<resultMap id="StudentWithTeacher" type="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<association property="teacher" javaType="Teacher">
<result property="name" column="tname"/>
</association>
</resultMap>
<!--
接下来我们需要对sql语句中的字段作映射:
1. 前面两个result标签,分别定义了查询的id, name分别对应了sql中的sid和sname
2. 然后我们使用了前面定义的association概念,从学生的角度和老师的关系是一个关联,
设置teacher对应的Java对象是Teacher, 然后设置teacher对象的name属性对应了sql中的tname
-->
这样我们再执行前面的测试方法,可以看到teacher的姓名正常的查询了出来:
10.4、按照查询嵌套处理(子查询方式)
StudentMapper.xml
<select id="getStudents2" resultMap="StudentWithTeacher2">
select * from student
</select>
<resultMap id="StudentWithTeacher2" type="Student">
<result property="id" column="id"/>
<result property="name" column="name"/>
<!-- 复杂的对象需要特殊处理:对象 association 集合 collection -->
<association property="teacher" javaType="Teacher" column="tid" select="getTeacher"/>
<!-- 同样的使用association关联,设置关联的对象是Teacher,对应的sql字段是tid,在select中还设置了另一个select,相当与子查询的语句 -->
</resultMap>
<select id="getTeacher" resultType="Teacher">
select * from teacher where id = #{id}
</select>
个人认为这种方式没有关联查询好理解。
11、一对多处理
环境准备
与多对一相对的,我们来看下一对多的处理方式。同样的,首先准备下环境信息,环境配置和上面多对一基本相同,有区别的是实体类的部分。
pojo/Student
@Data
public class Student {
private int id;
private String name;
private int tid;
}
pojo/Teacher
@Data
public class Teacher {
private int id;
private String name;
private List<Student> students;
}
写一个简单的测试方法,确保可以正常查询数据,即可继续下面的内容。
业务场景
我们的业务场景是:从Teacher表中查询老师的信息,并且将老师的所有学生信息都查询出来。
首先写一个最简单的查询,从老师表中查询出所有的老师信息:
TeacherMapper.java
Teacher getTeacherById(@Param("tid") int id);
TeacherMapper.xml
<select id="getTeacherById" resultType="Teacher">
select * from teacher where id = ${tid};
</select>
MyTest.java
@Test
public void test(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
TeacherMapper mapper = sqlSession.getMapper(TeacherMapper.class);
Teacher teacher = mapper.getTeacherById(1);
System.out.println(teacher);
sqlSession.close();
}
这段代码都已经相当熟悉了,我们看查询出的结果:
可以看到和预期的结果一样,老师的信息查询了出来,但是其中学生的信息显示为null,当然接下里就是处理学生为null的问题。
按照查询嵌套处理
和多对一的时候一样,直接上代码:
TeacherMapper.xml
<select id="getTeacherById" resultMap="TeacherStudent">
select t.id tid, t.name tname, s.id sid, s.name sname
from teacher t, student s
where t.id = s.tid
and t.id = #{tid}
</select>
<resultMap id="TeacherStudent" type="Teacher">
<result property="id" column="tid"/>
<result property="name" column="tname"/>
<collection property="students" ofType="Student">
<result property="id" column="sid"/>
<result property="name" column="sname"/>
<result property="tid" column="tid"/>
</collection>
</resultMap>
查询结果,学生信息正确的查询了出来:
其中需要注意的是:
- ResultMap中的collection,因为这里一个老师下有很多的学生,所以使用集合
collection
来表示 - 由于collection中对应的是一个Student集合(即一个列表),在这里没有用JavaType来指定实体类型,改成使用ofType来实现
按照结果结果嵌套查询
TeacherMapper.java
Teacher getTeacherById2(@Param("tid") int id);
TeacherMapper.xml
<select id="getTeacherById2" resultMap="TeacherStudent2">
select * from teacher where id = #{tid};
</select>
<resultMap id="TeacherStudent2" type="Teacher">
<collection property="students" javaType="ArrayList" ofType="Student"
select="getStudentByTeacherId" column="id"/>
</resultMap>
<select id="getStudentByTeacherId" resultType="Student">
select * from student where tid = #{tid}
</select>
使用上看子查询的方式也可以正常查询出来。
小结
- 关联:association
- 集合:collection
- javaType & ofType
- javaType用来指定实体类中属性的类型
- ofType用来指定映射到List或者集合中的pojo类型,泛型中的约束类型。
注意点:
- 保证SQL的可读性,尽量保证通俗易懂
- 注意多对一和一对多种,属性名和字段的问题
- 问题排查可借助日志,建议使用Log4j