10.1.2 一对多

10.1.2 一对多

在实际项目开发中,一对多是非常常见的关系,比如,一个班级可以有多个学生,一个学生只能属于一个班级,班级和学生之间是一对多的关系,而学生和班级之间是多对一的关系。

多方维护关系

在数据库中一对多关系通常使用主外键关联,外键列应该在多方,即多方维护关系。下面我们就用一个示例来看看MyBatis怎么处理一对多关系。

示例:OneToManyTest

项目结构

展开/折叠
D:\Desktop\随书源码\Spring+Mybatis企业应用实战(第2版)\codes\10\OneToManyTest
├─src\
│ ├─db.properties
│ ├─log4j.xml
│ ├─mybatis-config.xml
│ ├─org\
│ │ └─fkit\
│ │   ├─domain\
│ │   │ ├─Clazz.java
│ │   │ └─Student.java
│ │   ├─factory\
│ │   │ └─FKSqlSessionFactory.java
│ │   ├─mapper\
│ │   │ ├─ClazzMapper.java
│ │   │ ├─ClazzMapper.xml
│ │   │ ├─StudentMapper.java
│ │   │ └─StudentMapper.xml
│ │   └─test\
│ │     └─OneToManyTest.java
│ └─tb_test.sql
└─WebContent\
  ├─META-INF\
  │ └─MANIFEST.MF
  └─WEB-INF\
    ├─lib\
    │ ├─ant-1.9.6.jar
    │ ├─ant-launcher-1.9.6.jar
    │ ├─asm-5.2.jar
    │ ├─cglib-3.2.5.jar
    │ ├─commons-logging-1.2.jar
    │ ├─javassist-3.22.0-CR2.jar
    │ ├─log4j-1.2.17.jar
    │ ├─log4j-api-2.3.jar
    │ ├─log4j-core-2.3.jar
    │ ├─mybatis-3.4.5.jar
    │ ├─mysql-connector-java-8.0.17.jar
    │ ├─ognl-3.1.15.jar
    │ ├─slf4j-api-1.7.25.jar
    │ └─slf4j-log4j12-1.7.25.jar
    └─web.xml

创建数据库表

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

/OneToManyTest/src/tb_test.sql
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
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=utf8;

-- 插入数据到tb_clazz表
INSERT INTO `tb_clazz` VALUES ('1', 'B151516', 'Java基础班');

-- 创建tb_student表
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=utf8;

-- 插入数据到tb_student
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');
-- 恢复外键检查
SET FOREIGN_KEY_CHECKS=1;

tb_student表的clazz_id作为外键参照tb_clazz表的主键id

mybatis数据库中执行SQL脚本,完成创建数据库和表的操作。

接下来,创建一个clazz对象和一个Student对象分别映射tb_clazztb_student表。

创建持久化对象

Student.java

/OneToManyTest/src/org/fkit/domain/Student.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.fkit.domain;
import java.io.Serializable;

public class Student implements Serializable
{
private static final long serialVersionUID = -8985198663152992443L;
private Integer id; // 学生id,主键
private String name; // 姓名
private String sex; // 性别
private Integer age; // 年龄
// 学生和班级是多对一的关系,即一个学生只属于一个班级
private Clazz clazz;
// 省略构造方法
// 此处省略getter和setter方法,请自己补上
// 省略toString()方法
}

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

Clazz.java

/OneToManyTest/src/org/fkit/domain/Clazz.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.fkit.domain;
import java.io.Serializable;
import java.util.ArrayList;
public class Clazz implements Serializable
{
private static final long serialVersionUID = 7120329740548835778L;
private Integer id; // 班级id,主键
private String code; // 班级编号
private String name; // 班级名称
// 班级和学生是一对多的关系,即一个班级可以有多个学生
private ArrayList<Student> students;
// 省略构造方法
// 省略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
<?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
column="id"
select="org.fkit.mapper.StudentMapper.selectStudentByClazzId"
property="students"
javaType="ArrayList"
ofType="org.fkit.domain.Student"
fetchType="lazy">
<id column="id" property="id" />
<result column="name" property="name" />
<result column="sex" property="sex" />
<result column="age" property="age" />
</collection>
</resultMap>
</mapper>

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

collection标签详解

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

MyBatis根配置文件中 开启懒加载

/OneToManyTest/src/mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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

/OneToManyTest/src/org/fkit/mapper/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
<?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接口

/OneToManyTest/src/org/fkit/mapper/ClazzMapper.java
1
2
3
4
public interface ClazzMapper {
// 根据id查询班级信息
Clazz selectClazzById(Integer id);
}

StudentMapper接口

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

测试类

/OneToManyTest/src/org/fkit/test/OneToManyTest.java
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package org.fkit.test;

import java.util.List;
import org.apache.ibatis.session.SqlSession;
import org.fkit.domain.Clazz;
import org.fkit.domain.Student;
import org.fkit.factory.FKSqlSessionFactory;
import org.fkit.mapper.ClazzMapper;
import org.fkit.mapper.StudentMapper;

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(多)的信息。

运行结果

testSelectStudentById

main方法中运行testSelectStudentById()方法,其通过SqlSessiongetMapper(Class<T> type)方法获得mapper接口的代理对象ClazzMapper,调用selectClazzById方法时会执ClazzMapper.xmlid="selectClazzById"select元素中定义的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基础班]

testSelectClazzById

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

控制台输出
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标签的属性

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

collection标签的子标签

  • id标签
  • result标签

association标签

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

association标签的属性

  • column属性指定要将哪一列,作为select标签关联的查询语句的参数。
  • select属性指定关联查询的select标签
  • property设置接收查询结果的持久化对象的属性
  • javaType设置该属性的类型

association标签的子标签

  • id标签
  • result标签