您现在的位置是:首页 >学无止境 >【Spring】— 动态SQL :MyBatis的关联映射网站首页学无止境
【Spring】— 动态SQL :MyBatis的关联映射
MyBatis的关联映射
在实际应用中,对数据库的操作会涉及多张表,这在面向对象中就涉及对象与对象之间的关联关系。针对多表之间的操作,MyBatis提供了关联映射,通过关联映射来处理对象与对象之间的关联关系。
1、关联关系概述
在关系型数据库中,多表之间存在3种关联关系,分别为一对一、一对多和多对多。
- 一对一:在任意一方引入对方主键作为外键。
- 一对多:在“多”的一方添加“一”的一方的主键作为外键。
- 多对多:产生中间关系表,引入两张表的主键作为外键,两个主键成为联合主键或使用新的字段作为主键。
对象之间也存在3种关联关系。
- 一对一的关系:在本类中定义对方类型的对象,比如A类中定义B类类型的属性b、在B类中定义A类类型的属性a。
- 一对多的关系:一个A类类型对应多个B类类型的情况,需要在A类中以集合的方式引入B类类型的对象,在B类中定义A类类型的属性a。
- 多对多的关系:在A类中定义B类类型的集合,在B类中定义A类类型的集合。
2、MyBatis中的关联关系
2.1 一对一
在现实生活中,一对一关联关系是十分常见的。例如,一个学生只有一本学生证,同时一本学生证也只对应一个学生。
那么MyBatis是怎么处理这种一对一关联关系的呢,之前学习的<resultMap>
元素中包含一个<association>
子元素,MyBatis就是通过该元素来处理一对一关联关系的。
在<association>
元素中,通常可以配置以下属性。
- property:指定映射到的实体类对象属性,与表字段一一对应。
- column:指定表中对应的字段。
- javaType:指定映射到实体对象属性的类型。
- select:指定引入嵌套查询的子SQL语句,用于关联映射中的嵌套查询。
- fetchType:指定在关联查询时是否启用延迟加载,有lazy和eager两个属性值,默认值为lazy(默认关联映射延迟加载)。
<association>
元素有如下两种配置方式。
注意
MyBatis在映射文件中加载关联关系对象主要通过两种方式:嵌套查询和嵌套结果。
嵌套查询是指通过执行另一条SQL映射语句来返回预期的复杂类型;
嵌套结果是使用嵌套结果映射来处理重复的联合结果的子集。
【示例9】接下来以学生和学生证之间的一对一关联关系为例。
查询学生及其关联的学生证信息是先通过查询学生表中的主键来获取学生信息,然后通过表中的外键来获取学生证表中的学生证号信息。其具体实现步骤如下。
步骤01 创建数据表。在db_mybatis数据库中分别创建名为tb_studentidcard和tb_student的数据表,同时预先插入几条数据。其执行的SQL语句如下所示。
#使用数据库db mybatis
USE db_mybatis;
#创建一个名称为tb_studentidcard的表
CREATE TABLE IF NOT EXISTS tb_studentidcard(
id INT PRIMARY KEY AUTO_INCREMENT,
CODE VARCHAR(8)
);
#插入两条数据
INSERT INTO tb_studentidcard(CODE) VALUES('18030128');
INSERT INTO tb_studentidcard(CODE) VALUES('18030135');
#创建一个名称为tb_student的表(暂时添加少量字段)
CREATE TABLE tb_student(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32),
sex CHAR(1),
card_id INT UNIQUE,
FOREIGN KEY (card_id) REFERENCES tb_studentidcard(id)
);
#插入两条数据
INSERT INTO tb_student (name,sex,card_id) VALUES ('limin','f',1);
INSERT INTO tb_student (name,sex,card_id) VALUES ('jack','m',2);
步骤02 创建一个名为chapter09的Web项目,然后引入相关JAR包、MybatisUtils工具类以及mybatis-config.xml核心配置文件。
步骤03 在项目的com.ssm.po包下创建持久化类:学生证类StudentIdCard和学生类Student。
//StudentIdCard.java
package com.ssm.po;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年06月08日 17:40
*/
public class StudentIdCard {
private Integer id;
private String code;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String toString() {
return "StudentIdCard [id=" + id + ", code= " + code + "]";
}
}
//Student.java
package com.ssm.po;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年06月08日 17:41
*/
public class Student {
private Integer id;
private String name;
private String sex;
private StudentIdCard studentIdCard;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
public StudentIdCard getStudentIdCard() {
return studentIdCard;
}
public void setStudentIdCard(StudentIdCard studentIdCard) {
this.studentIdCard = studentIdCard;
}
@Override
public String toString() {
return "Student[id=" + id + ", name=" + name + ", sex=" + sex + ", studentIdCard=" + studentIdCard +"]";
}
}
步骤04 在com.ssm.mapper包中创建学生证映射文件StudentIdCardMapper.xml和学生映射文件StudentMapper.xml,并在两个映射文件中编写一对一关联映射查询的配置信息。
StudentIdCardMapper.xml
<?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="com.ssm.mapper.StudentIdCardMapper">
<!-- 根据学生id获取学生证信息-->
<select id="findStudentIdCardById" parameterType="com.ssm.po.StudentIdCard" resultType="com.ssm.po.StudentIdCard">
select * from tb_studentidcard where id = #{id}
</select>
</mapper>
StudentMapper.xml
<?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="com.ssm.mapper.StudentMapper">
<!-- 根据学生id获取学生证信息-->
<select id="findStudentById" parameterType="com.ssm.po.Student" resultMap="CardId">
select * from tb_student where id = #{id}
</select>
<resultMap id="CardId" type="Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="sex" column="sex"/>
<!-- 一对一,association使用select属性引入另一条SQL语句 -->
<association property="studentIdCard" column="card_id" javaType="StudentIdCard"
select="com.ssm.mapper.StudentIdCardMapper.findStudentIdCardById"/>
</resultMap>
</mapper>
在上述两个映射文件中使用了MyBatis中的嵌套查询方式进行学生及其关联的学生证信息查询,因为返回的学生对象中除了基本属性外,还有一个关联的studentIdCard属性,所以需要手动编写结果映射。从映射文件StudentMapper.xml中可以看出,嵌套查询的方法是先执行一个简单的SQL语句,然后在进行结果映射时将关联对象在<association>
元素中使用select属性执行另一条SQL语句(StudentIdCardMapper.xml中的SQL)。
步骤05 在核心配置文件mybatis-config.xml中引入Mapper映射文件并定义别名,如下所示。
mybatis-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 引入数据库连接配置文件-->
<!-- <properties resource="db.properties"/>-->
<!-- 使用扫描包的形式定义别名-->
<typeAliases>
<package name="com.ssm.po"/>
</typeAliases>
<environments default="mysql">
<!-- 1.2 配置id为mysql 的数据库环境-->
<environment id="mysql">
<!-- 使用JDBC的事务管理-->
<transactionManager type="JDBC" />
<!-- 数据库连接池-->
<dataSource type="POOLED">
<!-- 数据库驱动-->
<property name="driver" value="com.mysql.jdbc.Driver" />
<!-- 连接数据库的url-->
<property name="url" value="jdbc:mysql://localhost:3306/db_mybatis" />
<!-- 连接数据库的用户名-->
<property name="username" value="root" />
<!-- 连接数据库的用户名-->
<property name="password" value="root" />
</dataSource>
</environment>
</environments>
<!--2.配置 Mapper的位置-->
<mappers>
<mapper resource="com/ssm/mapper/UserMapper.xml" />
<mapper resource="com/ssm/mapper/StudentMapper.xml" />
<mapper resource="com/ssm/mapper/StudentIdCardMapper.xml" />
</mappers>
</configuration>
在上述核心配置文件中,首先引入了数据库连接的配置文件,然后使用扫描包的形式自定义别名,接下来进行环境的配置,最后配置了Mapper映射文件的位置信息。
步骤06 在com.ssm.test包中创建测试类MybatisAssociatedTest,并在类中编写测试方法findStudentByIdTest(),如文件所示。
//MybatisAssociatedTest.java
package com.ssm.test;
import com.ssm.po.Student;
import com.ssm.po.User;
import com.ssm.util.MybatisUtil;
import org.apache.ibatis.session.SqlSession;
import org.junit.Test;
import java.util.List;
public class MybatisAssociatedTest {
/*
*嵌套查询
*/
@Test
public void findStudentByIdTest() {
//通过工具类生成SqlSession对象
SqlSession sqlSession = MybatisUtil.getSession();
//执行SqlSession的查询办法,返回结果集
Student student =
sqlSession.selectOne("com.ssm.mapper.StudentMapper.findStudentById",1);
System.out.println(student.toString());
sqlSession.close();
}
}
在findStudentByIdTest()方法中,首先通过MybatisUtils工具类获取了SqlSession对象,然后通过SqlSession对象的selectOne()方法获取了学生信息,最后关闭了SqlSession。执行方法后,控制台的输出结果如图所示。使用MyBatis嵌套查询的方式查询出了学生及其关联的学生证信息,这就是MyBatis中的一对一关联查询。
虽然使用嵌套查询的方式比较简单,但是嵌套查询的方式要执行多条SQL语句,这对于大型数据集合和列表展示不是很好,因为这样可能会导致成百上千条关联的SOL语句被执行,从而极大地消耗数据库性能,并且会降低查询效率。为此,MyBatis提供了嵌套结果的方式进行关联查询。
在StudentMapper.xml中,使用MyBatis嵌套结果的方式进行学生及其关联的学生证信息查询,所添加的代码如下所示。
<?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="com.ssm.mapper.StudentMapper">
<!-- 嵌套结果,通过嵌套结果映射来处理重复的联合结果的子集-->
<select id="findStudentById2" parameterType="Integer" resultMap="CardId2">
select s.* ,sidcard.code
from tb_student s,tb_studentidcard sidcard
where s.card_id = sidcard.id and s.id=#{id}
</select>
<resultMap id="CardId2" type="Student">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="sex" column="sex"/>
<!-- 一对一,association使用select属性引入另一条SQL语句 -->
<association property="studentIdCard" javaType="StudentIdCard">
<id property="id" column="card_id"/>
<result property="code" column="code"/>
</association>
</resultMap>
</mapper>
从上述代码中可以看出,MyBatis嵌套结果的方式只编写了一条复杂的多表关联的SQL语句,并且在<association>
元素中继续使用相关子元素进行数据库表字段和实体类属性的一一映射。执行结果相同,但使用MyBatis嵌套结果的方式只执行了一条SQL语句。
注意
在使用MyBatis嵌套查询方式进行关联查询映射时,使用MyBatis的延迟加载在一定程度上可以降低运行消耗并提高查询效率。
MyBatis默认没有开启延迟加载,需要在核心配置文件mybatis-config.xml中的<settings>
元素内进行配置,具体配置方式如下。
在映射文件中,MyBatis关联映射的<association>
元素和<collection>
元素中都已默认配置了延迟加载属性,即默认属性fetchType=“lazy”(属性fetchType="eager"表示立即加载),所以在配置文件中开启延迟加载后,无须在映射文件中再做配置。
2.2 一对多
在实际应用中,应用更多的关联关系是一对多(或多对一)。例如,一个班级有多个学生,即多个学生属于一个班级。使用MyBatis是怎么处理这种一对多关联关系的呢?之前所学的<resultMap>
元素中包含一个<collection>
子元素,MyBatis就是通过该元素来处理一对多关联关系的。<collection>
子元素的属性大部分与<collection>
元素相同,但其还包含一个特殊属性—ofType。ofType属性与javaType属性对应,用于指定实体对象中集合类属性所包含的元素类型。
<collection>
元素可以参考如下两种示例进行配置,具体代码如下。
【示例】在了解了MyBatis处理一对多关联关系的元素和方式后,接下来以班级和学生之间的这种一对多关联关系为例学习如何在MyBatis中处理一对多关联关系,具体步骤如下。
步骤01 在db_mybatis数据库中创建两个数据表:tb_banji和tb_student,同时在表中预先插入几条数据,执行的SQL语句如下所示。
#创建一个名称为tb_banji的表(暂添加少量字段)
CREATE TABLE tb_banji(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32)
);
#插入两条数据
INSERT INTO tb_banji VALUES(1, '16软件技术1班');
INSERT INTO tb_banji VALUES(2, '16软件技术2班');
#创建一个名称为tb_student的表(暂时添加少量字段)
CREATE TABLE tb_student(
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32),
sex CHAR(1),
banji_id INT,
FOREIGN KEY (banji_id) REFERENCES tb_banji (id)
);
#插入3条数据
INSERT INTO tb_student VALUES(1,'孙蕙','m',1);
INSERT INTO tb_student VALUES(2,'刘梦奕','f',1);
INSERT INTO tb_student VALUES(3,'无为','m',2);
步骤02 在com.ssm.po包中创建持久化类:班级类Banji和学生类StudentOne,并在两个类中定义相关属性和方法,如文件9.7和文件9.8所示。
//Banji.java
package com.ssm.po;
import java.util.List;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年06月09日 16:07
*/
public class Banji {
private Integer id;
private String name;
private List<Student> studentList;
public Integer getId() {
return id;
}
public void setId(Integer id){
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Student> getStudentList() {
return studentList;
}
public void setStudentList(List<Student> studentList) {
this.studentList = studentList;
}
public String toString() {
return "Banji [id=" + id + ",name=" + name + ",studentList=" + studentList + "]";
}
}
//StudentOne.java
package com.ssm.po;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年06月09日 16:14
*/
public class StudentOne {
private Integer id;
private String name;
private String sex;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
@Override
public String toString() {
return "StudentOne [id=" + id + ", name=" + name + ", sex=" + sex + "]";
}
}
步骤03 在com.ssm.mapper包中创建班级实体映射文件BanjiMapper.xml,并在文件中编写一对多关联映射查询的配置,如文件所示。
<?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="com.ssm.mapper.BanjiMapper">
<!--一对多,查看某一班级及其关联的学生信息
注意:若关联查出的列名相同,则需要使用别名区分-->
<select id="findBanjiWithStudent" parameterType="Integer" resultMap="BanjiWithStudentResult">
select b.* ,s.id as student_id,s.name
from tb_banji b,tb_student s
where b.id = s.banji_id and b.id=#{id}
</select>
<resultMap id="BanjiWithStudentResult" type="Banji">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!--一对多关系映射:collection
ofType表示属性集合中的元素的类型List<Student>属性,即Student类 -->
<collection property="studentList" ofType="Student">
<id property="id" column="student_id"/>
<result property="name" column="name"/>
<result property="sex" column="sex"/>
</collection>
</resultMap>
</mapper>
在文件中使用MyBatis嵌套结果的方式定义了一个根据班级id查询班级及其关联的学生信息的select语句。因为返回的班级对象中包含Student集合对象属性,所以需要手动编写结果映射信息。
步骤04 将映射文件BanjiMapper.xml的路径配置到核心配置文件mybatis-config.xml中,其代码如下所示。
<mapper resource="com/ssm/mapper/BanjiMapper.xml" />
步骤05 在测试类MyBatisAssociatedTest中编写测试方法findBanjiTest()。
@Test
public void findStudentByIdTest() {
//通过工具类生成SqlSession对象
SqlSession sqlSession = MybatisUtil.getSession();
//执行SqlSession的查询办法,返回结果集
Banji banji =
sqlSession.selectOne("com.ssm.mapper.BanjiMapper.findBanjiWithStudent",1);
System.out.println(banji.toString());
sqlSession.close();
}
执行方法后,控制台输出结果如图所示。使用MyBatis嵌套结果的方式查询出了班级及其关联的学生集合信息。这就是MyBatis一对多的关联查询。
注意
上述案例从班级的角度出发,班级与学生之间是一对多的关联关系,但如果从单个学生的角度出发,一个学生只能属于一个班级,即一对一的关联关系。
2.3 多对多
在实际项目开发中,多对多的关联关系是非常常见的。以学生和课程为例,一个学生可以选修多门课程,而一门课程又可以被多个学生选修,学生和课程就属于多对多的关联关系。
在数据库中,多对多的关联关系通常使用一个中间表来维护,中间表选课表(electiveCourse)中的学生id(student_id)作为外键参照学生表的id,课程id(course_id)作为外键参照课程表的id。三个表的关联关系如图所示。
【示例】了解了数据库中学生表与课程表之间的多对多关联关系后,下面我们通过具体的案例来讲解如何使用MyBatis处理这种多对多的关系,具体实现步骤如下。
步骤01 创建数据表。在mybatis数据库中新建名称为tb_course和tb_electiveCourse的两个数据表(tb_student表已在前面的案例中创建,这里直接引用),同时在表中预先插入几条数据。其执行的SQL语句如下所示。
#创建一个名称为tb_course的表
CREATE TABLE tb_course (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32),
code VARCHAR(32)
);
#插入两条数据
INSERT INTO tb_course VALUES (1, 'Java程序设计语言','08113226');
INSERT INTO tb_course VALUES (2, 'Javaweb程序开发入门','08113228');
#创建一个名称为tb_electiveCourse的中间表
CREATE TABLE tb_electiveCourse (
id INT PRIMARY KEY AUTO_INCREMENT,
student_id INT,
course_id INT,
FOREIGN KEY (student_id) REFERENCES tb_student(id),
FOREIGN KEY (course_id) REFERENCES tb_course(id)
);
#插入3条数据
INSERT INTO tb_electiveCourse VALUES (1,1,1);
INSERT INTO tb_electiveCourse VALUES (2,1,2);
INSERT INTO tb_electiveCourse VALUES (3,2,2);
步骤02 在com.ssm.po包中创建持久化类课程类Course,并在类中定义相关属性和方法,如文件所示。
//Course.java
package com.ssm.po;
import java.util.List;
/**
* 功能描述
*
* @author: 衍生星球
* @date: 2023年06月11日 23:01
*/
public class Course {
private Integer id;
private String name;
private String code;
private List<Student> studentlist;//于学生集合的关联属性
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public List<Student> getStudentlist() {
return studentlist;
}
public void setStudentlist(List<Student> studentlist) {
this.studentlist = studentlist;
}
@Override
public String toString() {
return "Course[id=" + id + ", name=" + name + ", code=" + code + "}";
}
}
除了需要在课程持久化类中添加学生集合的属性外,还需要在学生持久化类(Student.java)中增加课程集合的属性及其对应的getter()/setter()方法,同时为了方便查看输出结果,需要重写toString()。Student类中添加的代码如下所示。
步骤03 在com.ssm.mapper包中创建课程实体映射文件CourseMapper.xml和学生实体映射文件StudentMapper.xml,对两个映射文件进行编辑后,如下文件所示。
//CourseMapper.xml
<?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="com.ssm.mapper.CourseMapper">
<!-- 多对多嵌套查询:通过执行一条SQL映射语句来返回预期的特殊类型-->
<select id="findCourseWithStudent" parameterType="Integer" resultMap="CourseWithStudentResult">
select * from tb_course where id = #{id}
</select>
<resultMap id="CourseWithStudentResult" type="Course">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="code" column="code"/>
<collection property="studentlist" column="id" ofType="Student"
select="com.ssm.mapper.StudentMapper.findStudentById">
</collection>
</resultMap>
</mapper>
使用嵌套查询的方式定义了一个id为findCourseWithStudent的select语句来查询课程及其关联的学生信息。在<resultMap>
元素中使用了<collection>
元素来映射多对多的关联关系,其中property属性表示订单持久化类中的课程属性,ofType属性表示集合中的数据为Student类型,而column的属性值会作为参数执行StudentMapper.xml中定义的id为findStudentById的执行语句来查询订单中的学生信息。
//StudentMapper.xml
<?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="com.ssm.mapper.StudentMapper">
<!-- 嵌套结果,通过嵌套结果映射来处理重复的联合结果的子集-->
<select id="findStudentById" parameterType="Integer" resultType="Student">
select * from tb_student where id in (
select student_id from tb_electivecourse where course_id=#{id}
)
</select>
</mapper>
其中定义了一个id为findStudentById的执行语句,该执行语句中的SQL会根据课程id查询与该课程所关联的学生信息。由于课程和学生是多对多的关联关系,因此需要通过中间表来查询学生信息。
步骤04 将新创建的映射文件CourseMapper.xml和StudentMapper.xml的文件路径配置到核心配置文件mybatis-config.xml中,代码如下所示。
<mapper resource="com/ssm/mapper/CourseMapper.xml" />
<mapper resource="com/ssm/mapper/StudentMapper.xml" />
步骤05 在测试类MyBatisAssociatedTest中编写多对多关联查询的测试方法findCourseByIdTest(),其代码如下所示。
/*
*多对多嵌套查询
*/
@Test
public void findCourseByIdTest() {
SqlSession sqlSession = MybatisUtil.getSession();
//查询id为1的课程中的学生信息
Course course =
sqlSession.selectOne("com.ssm.mapper.CourseMapper.findCourseWithStudent",1);
System.out.println(course);
sqlSession.close();
}
使用MyBatis嵌套查询的方式查询出了课程及其关联的学生信息,这就是MyBatis多对多的关联查询。
如果多表关联查询的SQL语句比较熟,就可以在CourseMapper.xml中使用嵌套结果的方式,其代码如下所示。