0%

10.1.2 一对多

10.1.2 一对多

在实际项目开发中,一对多是非常常见的关系,比如,一个班级可以有多个学生个学生只能属于一个班级,班级和学生之间是一对多的关系,而学生和班级之间是多对的关系。在数据库中一对多关系通常使用主外键关联,外键列应该在多方,即多方维护关系。下面我们就用一个示例来看看MyBatis怎么处理一对多关系。

示例:OneToManyTest

创建数据库表

首先,给之前创建的mybatis数据库创建两个表tb_clazztb_student,并插入测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use mybatis;
# 取消外键检查
SET FOREIGN_KEY_CHECKS=0;
# 创建tb_clazz表
DROP TABLE IF EXISTS `tb_clazz`;
CREATE TABLE `tb_clazz` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`code` varchar(18) NOT NULL,
`name` varchar(18) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
# 插入数据到tb_clazz表
INSERT INTO `tb_clazz` VALUES ('1', 'B151516', 'Java基础班');

DROP TABLE IF EXISTS `tb_student`;
CREATE TABLE `tb_student` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(18) DEFAULT NULL,
`sex` varchar(18) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`clazz_id` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `clazz_id` (`clazz_id`),
CONSTRAINT `tb_student_ibfk_1` FOREIGN KEY (`clazz_id`)
REFERENCES `tb_clazz` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `tb_student` VALUES ('1', '小明', '男', '24', '1');
INSERT INTO `tb_student` VALUES ('2', '小王', '男', '23', '1');
INSERT INTO `tb_student` VALUES ('3', '小芳', '女', '22', '1');
INSERT INTO `tb_student` VALUES ('4', '小丽', '女', '22', '1');

tb_student表的clazz_id作为外键参照tb_clazz表的主键id.
mybatis数据库中执行SQL脚本,完成创建数据库和表的操作。
接下来,创建一个clazz对象和一个Student对象分别映射tb_clazztb_student表.

创建持久化对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Student
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 学生id,主键
private String name; // 姓名
private String sex; // 性别
private Integer age; // 年龄
// 学生和班级是多对一的关系,即一个学生只属于一个班级
private Clazz clazz;
public Student()
{
super();
}
// 此处省略getter和setter方法,请自己补上
// 省略toStrring方法.
}

学生和班级之间是多对一的关系,即一个学生只属于一个班级。在Student类当中定义了一个clazz属性,该属性是一个Clazz类型,用来映射多对一的关联关系,表示该学生所属的班级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Clazz
implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer id; // 班级id,主键
private String code; // 班级编号
private String name; // 班级名称
// 班级和学生是一对多的关系,即一个班级可以有多个学生
private List<Student> students;
public Clazz()
{
super();
}
// 此处省略getter和setter方法,请自己补上
// 此处省略toString方法.
}

班级和学生之间是一对多的关系,即一个班级可以有多个学生。在Clazz类当中定义了一个students属性,该属性是一个List集合,用来映射一对多的关联关系,表示一个班级有多个学生.

创建XML映射文件

再接下来是XML映射文件。

ClazzMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.ClazzMapper">
<!-- 根据id查询班级信息,返回resultMap -->
<select
id="selectClazzById"
parameterType="int"
resultMap="clazzResultMap"> SELECT * FROM tb_clazz WHERE id = #{id}
</select>
<!-- 映射Clazz对象的resultMap -->
<resultMap
type="org.fkit.domain.Clazz"
id="clazzResultMap">
<id
property="id"
column="id"/>
<result
property="code"
column="code"/>
<result
property="name"
column="name"/>
<!-- 一对多关联映射:collection fetchType="lazy"表示懒加载 -->
<collection
property="students"
javaType="ArrayList"
ofType="org.fkit.domain.Student"
column="id"
select="org.fkit.mapper.StudentMapper.selectStudentByClazzId"
fetchType="lazy">
<id
property="id"
column="id"/>
<result
property="name"
column="name"/>
<result
property="sex"
column="sex"/>
<result
property="age"
column="age"/>
</collection>
</resultMap>
</mapper>

ClazzMapper.xml中定义了一个select标签,其根据id查询班级信息。由于Clazz类除了简单的属性idcodename以外,还有一个关联对象students,所以返回的是一个id为clazzResultMapresultMap。由于students是一个List集合,所以clazzResultMap使用了collection元素映射一对多的关联关系,

collection标签详解

属性
  • property属性用于指定一个持久化对象的属性,select语句的查询的结果将会封装到这个属性中.
  • javaType属性用于指定属性的类型,也就是students变量的类型是ArrayList类型.
  • ofType属性用于指定属性的位置(也就是指定属性定义的类),ofType="org.fkit.domain.Student"表示students属性是org.fkit.domain.Student的属性。
  • column属性要查询的数据库表的列名
  • select属性表示会使用column属性值id作为参数执行StudentMapper中定义的idselectStudentByClazzIdselect标签,以便查询该班级对应的所有学生数据,
  • 查询出的数据将被封装到property表示的students对象当中。
  • 还使用了一个新的属性fetchType,该属性的取值有eagerlazy,
    • eager表示立即加载,即查询Clazz对象的时候,会立即执行关联的selectStudentByClazzId中定义的SQL语句去査询班级的所有学生;
    • lazy表示懒加载,其不会立即发送SQL语句去查询班级的所有学生,而是等到需要使用到班级的students属性时,才会发送SQL语句去查询班级的所有学生。
    • fetch机制更多的是为了性能考虑,
      • 如果查询班级时确定会访问班级的所有学生则该属性应该被设置为eager;
      • 如果査询班级时只是查询班级信息,有可能不会访问班级的所有学生,则该属性应该被设置为lazy
      • 正常情况下,一对多所关联的集合对象,都应该被设置成lazy

MyBatis根配置文件中支持懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<!-- XML 配置文件包含对 MyBatis 系统的核心设置 -->
<configuration>
<!-- 省略其他配置...... -->
<!-- 指定 MyBatis 所用日志的具体实现 -->
<settings>
<!-- 省略其他配置...... -->
<!-- 要使延迟加载生效必须配置下面两个属性 -->
<setting
name="lazyLoadingEnabled"
value="true"/>
<setting
name="aggressiveLazyLoading"
value="false"/>
</settings>
<!-- 省略其他配置...... -->
</configuration>

设置说明

  • lazyLoadingEnabled属性表示延迟加载的全局开关。当开启时,所有关联对象都会延迟加载。默认为false。这里设为true表示延迟加载关联对象.
  • aggressiveLazyLoading属性启用时,会使带有延迟加载属性的对象立即加载;反之,每种属性将会按需加载。默认为true,所以这里需要设置成false,表示按需加载。

StudentMapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace指用户自定义的命名空间。 -->
<mapper namespace="org.fkit.mapper.StudentMapper">
<!-- 根据id查询学生信息,多表连接,返回resultMap -->
<select
id="selectStudentById"
parameterType="int"
resultMap="studentResultMap"> SELECT * FROM tb_clazz c,tb_student s WHERE c.id = s.clazz_id
AND s.id = #{id}
</select>
<!-- 根据班级id查询学生信息,返回resultMap -->
<select
id="selectStudentByClazzId"
parameterType="int"
resultMap="studentResultMap"> SELECT * FROM tb_student WHERE clazz_id = #{id}
</select>
<!-- 映射Student对象的resultMap -->
<resultMap
type="org.fkit.domain.Student"
id="studentResultMap">
<id
property="id"
column="id"/>
<result
property="name"
column="name"/>
<result
property="sex"
column="sex"/>
<result
property="age"
column="age"/>
<!-- 多对一关联映射:association -->
<association
property="clazz"
javaType="org.fkit.domain.Clazz">
<id
property="id"
column="id"/>
<result
property="code"
column="code"/>
<result
property="name"
column="name"/>
</association>
</resultMap>
</mapper>

StudentMapper.xml中定义了一个id="selectStudentById"select标签,其会根据学生id查询学生信息,由于Student类除了简单的属性idnamesexage之外,还有一个关联对象clazz,所以它返回的是一个名为studentResultMapresultMap.

提示

在实际开发中,一对多关系通常映射为集合对象,而由于多方的数据量可能很大,所以通常使用懒加载;而多对一只是关联到一个对象,所以通常使用多表连接直接把数据提取出来。

创建mapper接口对象

ClazzMapper接口

1
2
3
4
public interface ClazzMapper {
// 根据id查询班级信息
Clazz selectClazzById(Integer id);
}

StudentMapper接口

1
2
3
4
5
6
7
8
public interface StudentMapper {
/**
* 根据id查询学生信息
* @param id
* @return Student对象.
*/
Student selectStudentById(Integer id);
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public class OneToManyTest {
public static void main(String[] args)
{
// 1.定义SqlSession变量
SqlSession sqlSession = null;
try
{
// 2.创建SqlSession实例
sqlSession = FKSqlSessionFactory.getSqlSession();
OneToManyTest t = new OneToManyTest();
t.testSelectClazzById(sqlSession);
// t.testSelectStudentById(sqlSession);
// 提交事务
sqlSession.commit();
} catch (Exception e)
{
// 回滚事务
sqlSession.rollback();
e.printStackTrace();
} finally
{
// 关闭SqlSession
if (sqlSession != null)
sqlSession.close();
}
}
// 测试一对多,查询班级Clazz(一)的时候级联查询学生Student(多)
public void testSelectClazzById(SqlSession sqlSession)
{
// 1.获得ClazzMapper接口的代理对象
ClazzMapper cm = sqlSession.getMapper(ClazzMapper.class);
// 2.调用接口的代理对象的selectClazzById方法
Clazz clazz = cm.selectClazzById(1);
// 查看查询到的clazz对象信息
System.out.println(clazz.getId() + " " + clazz.getCode() + " " + clazz.getName());
// 查看clazz对象关联的学生信息
List<Student> students = clazz.getStudents();
students.forEach(stu -> System.out.println(stu));
}
// 测试多对一,查询学生Student(多)的时候级联查询 班级Clazz(一)
public void testSelectStudentById(SqlSession sqlSession)
{
// 获得StudentMapper接口的代理对象
StudentMapper sm = sqlSession.getMapper(StudentMapper.class);
// 调用selectStudentById方法
Student stu = sm.selectStudentById(1);
// 查看查询到的Student对象信息
System.out.println(stu);
// 查看Student对象关联的班级信息
System.out.println(stu.getClazz());
}
}

OneToManyTest类中定义了一个testSelectClazzById()方法,该方法用于测试一对多关系,查询班级Clazz(一)的时候关联查询学生Student(多)的信息。

运行结果

main方法中运行testSelectStudentById()方法,其通过SqlSessiongetMapper(Class<T> type)方法获得mapper接口的代理对象ClazzMapper,调用selectClazzById方法时会执ClazzMapper.xmlselectid="selectClazzById"的元素中定义的SQL语可。控制台显示如下:

1
2
3
4
5
DEBUG [main] ==>  Preparing: SELECT * FROM tb_clazz c,tb_student s WHERE c.id = s.clazz_id AND s.id = ? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
Student [id=1, name=Java基础班, sex=男, age=24]
Clazz [id=1, code=B151516, name=Java基础班]

main方法中运行testSelectStudentById()方法,控制台显示如下:

1
2
3
4
5
6
7
8
9
10
11
DEBUG [main] ==>  Preparing: SELECT * FROM tb_clazz WHERE id = ? 
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 1
1 B151516 Java基础班
DEBUG [main] ==> Preparing: SELECT * FROM tb_student WHERE clazz_id = ?
DEBUG [main] ==> Parameters: 1(Integer)
DEBUG [main] <== Total: 4
Student [id=1, name=小明, sex=男, age=24]
Student [id=2, name=小王, sex=男, age=23]
Student [id=3, name=小芳, sex=女, age=22]
Student [id=4, name=小丽, sex=女, age=22]

可以看到,MyBatis执行了一个多表查询语句,并且将查询到的班级信息封装到了学生对象的关联属性中。

总结

collection标签

collection标签的属性

-property属性指定集合的变量名
-javaType属性指定集合的类型
-ofType属性指定集合中存放的元素的类型
-select属性指定关联查询的select标签
-column属性指定要将哪一列,作为select标签关联的查询语句的参数.

collection标签的子标签

这些子标签表示集合中元素的各个属性.

association标签

对于多对一,一对一联系,使用多表连接查询即可.

association标签的属性

  • property设置持久化对象的属性
  • javaType设置该属性的类型

association标签的子标签

这些子标签表示关联的持久化对象的各个属性.

原文链接: 10.1.2 一对多