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_clazz
和tb_student
,并插入测试数据:
/OneToManyTest/src/tb_test.sql1 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;
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;
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=utf8;
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_clazz
和tb_student
表。
创建持久化对象
Student.java
/OneToManyTest/src/org/fkit/domain/Student.java1 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; private String name; private String sex; private Integer age; private Clazz clazz; }
|
学生和班级之间是多对一的关系,即一个学生只属于一个班级。在Student
类当中定义了一个clazz
属性,该属性是一个Clazz
类型,用来映射多对一的关联关系,表示该学生所属的班级。
Clazz.java
/OneToManyTest/src/org/fkit/domain/Clazz.java1 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; private String code; private String name; private ArrayList<Student> students; }
|
班级和学生之间是一对多的关系,即一个班级可以有多个学生。在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">
<mapper namespace="org.fkit.mapper.ClazzMapper"> <select id="selectClazzById" parameterType="int" resultMap="clazzResultMap"> SELECT * FROM tb_clazz WHERE id = #{id} </select> <resultMap type="org.fkit.domain.Clazz" id="clazzResultMap"> <id property="id" column="id" /> <result property="code" column="code" /> <result property="name" column="name" /> <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
类除了简单的属性id
、code
、name
以外,还有一个关联对象students
,所以返回的是一个id为clazzResultMap
的resultMap
。由于students
是一个List
集合,所以clazzResultMap
中使用了collection
元素映射一对多的关联关系。
collection标签详解
属性
column
属性要查询的数据库表的列名。
select
属性表示会使用column
属性值id
作为参数执行StudentMapper
中定义的id
为selectStudentByClazzId
的select
标签,以便查询该班级对应的所有学生数据,
property
属性用于指定一个持久化对象的属性,select
语句的查询的结果将会封装到这个属性中。
javaType
属性用于指定属性的类型,也就是students
变量的类型是ArrayList
类型。
ofType
属性用于指定属性的位置(也就是指定属性定义的类),ofType="org.fkit.domain.Student"
表示students
属性是org.fkit.domain.Student
的属性。查询出的数据将被封装到property
表示的students
对象当中。
- 还使用了一个新的属性
fetchType
,该属性的取值有eager
和lazy
,
eager
表示立即加载,即查询Clazz
对象的时候,会立即执行关联的selectStudentByClazzId
中定义的SQL
语句去査询班级的所有学生;
lazy
表示懒加载,其不会立即发送SQL
语句去查询班级的所有学生,而是等到需要使用到班级的students
属性时,才会发送SQL
语句去查询班级的所有学生。
fetch
机制更多的是为了性能考虑,
- 如果查询班级时确定会访问班级的所有学生则该属性应该被设置为
eager
;
- 如果査询班级时只是查询班级信息,有可能不会访问班级的所有学生,则该属性应该被设置为
lazy
。
- 正常情况下,一对多所关联的集合对象,都应该被设置成
lazy
。
MyBatis根配置文件中 开启懒加载
/OneToManyTest/src/mybatis-config.xml1 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">
<configuration> <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.xml1 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">
<mapper namespace="org.fkit.mapper.StudentMapper"> <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> <select id="selectStudentByClazzId" parameterType="int" resultMap="studentResultMap"> SELECT * FROM tb_student WHERE clazz_id = #{id} </select> <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 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
类除了简单的属性id
、name
、sex
和age
之外,还有一个关联对象clazz
,所以它返回的是一个名为studentResultMap
的resultMap
。
提示
在实际开发中,一对多
关系通常映射为集合对象
,而由于多方的数据量可能很大,所以通常使用懒加载;而多对一
只是关联到一个对象
,所以通常使用多表连接直接把数据提取出来。
创建mapper接口对象
ClazzMapper接口
/OneToManyTest/src/org/fkit/mapper/ClazzMapper.java1 2 3 4
| public interface ClazzMapper { Clazz selectClazzById(Integer id); }
|
StudentMapper接口
/OneToManyTest/src/org/fkit/mapper/StudentMapper.java1 2 3 4 5 6 7 8
| public interface StudentMapper {
Student selectStudentById(Integer id); }
|
测试类
/OneToManyTest/src/org/fkit/test/OneToManyTest.java1 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) { SqlSession sqlSession = null; try { sqlSession = FKSqlSessionFactory.getSqlSession(); OneToManyTest t = new OneToManyTest(); t.testSelectClazzById(sqlSession); sqlSession.commit(); } catch (Exception e) { sqlSession.rollback(); e.printStackTrace(); } finally { if (sqlSession != null) sqlSession.close(); } }
public void testSelectClazzById(SqlSession sqlSession) { ClazzMapper cm = sqlSession.getMapper(ClazzMapper.class); Clazz clazz = cm.selectClazzById(1); System.out.println(clazz.getId() + " " + clazz.getCode() + " " + clazz.getName()); List<Student> students = clazz.getStudents(); students.forEach(stu -> System.out.println(" " + stu)); }
public void testSelectStudentById(SqlSession sqlSession) { StudentMapper sm = sqlSession.getMapper(StudentMapper.class); Student stu = sm.selectStudentById(1); System.out.println(stu); System.out.println(stu.getClazz()); }
}
|
在OneToManyTest
类中定义了一个testSelectClazzById()
方法,该方法用于测试一对多关系,查询班级Clazz
(一)的时候关联查询学生Student
(多)的信息。
运行结果
testSelectStudentById
在main
方法中运行testSelectStudentById()
方法,其通过SqlSession
的getMapper(Class<T> type)
方法获得mapper
接口的代理对象ClazzMapper
,调用selectClazzById
方法时会执ClazzMapper.xml
中id="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标签的子标签
association标签
对于多对一,一对一联系,使用多表连接查询
即可。
association标签的属性
column
属性指定要将哪一列,作为select
标签关联的查询语句的参数。
select
属性指定关联查询的select
标签
property
设置接收查询结果的持久化对象的属性
javaType
设置该属性的类型
association标签的子标签