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标签的子标签