✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。
🍎个人主页:Java Fans的博客
🍊个人信条:不迁怒,不贰过。小知识,大智慧。
💞当前专栏:SSM 框架从入门到精通
✨特色专栏:国学周更-心性养成之路
🥭本文内容:MyBatis 多表关联查询
一对多关联查询是指在查询一方对象的时候,同时将其所关联的多方对象也都查询出来。下面以班级 Classes 与学生 Student 间的一对多关系为例进行演示。一个班级有多个学生,一个学生只属于一个班级。数据库 student 表里面有个字段 classno 是外键,对应主键表 Class 的主键 cid。
项目案例:查询班级号为 1801 的班级,同时遍历该班级的所有的学生信息
实现步骤:
【1】在 MySQL 中创建数据库 studentdb,创建表 student 和classes,并添加若干测试用的数据记录,SQL 语句如下:
CREATE DATABASE studentdb;
USE studentdb ;
DROP TABLE IF EXISTS student ;
CREATE TABLE student (id INT(11) NOT NULL,studentname VARCHAR(20) DEFAULT NULL,gender CHAR(2) DEFAULT NULL,age INT(11) DEFAULT NULL,classno VARCHAR(10),PRIMARY KEY ( id )
)
INSERT INTO student ( id , studentname , gender , age , classno ) VALUES (1,'张飞','男',18,'201801'),(2,'李白','男',20,'201801'),(3,'张无忌','男',19,'201801'),(4,'赵敏','女',17,'201801');CREATE TABLE classes (cid VARCHAR (30),cname VARCHAR (60)
);
INSERT INTO classes (cid, cname) VALUES('201801','计算机软件1班');
INSERT INTO classes (cid, cname) VALUES('201802','计算机软件2班');
【2】创建实体类 Classes 和 Student 类
Student 类如下:
package cn.kgc.my01.entity;import lombok.Data;@Data
public class Student {private String sid;private String sname;private String sex;private Integer age;//添加额外属性:所在班级private Classes classes;public String show(){return "学生编号:"+getSid()+",学生姓名:"+getSname()+",学生性别:"+getSex()+",学生年龄:"+getAge();}
}
Classes 类如下:
package cn.kgc.my01.entity;import lombok.Data;import java.util.List;@Data
public class Classes {private String cid;private String cname;//添加额外属性private List students;public String show(){return "班级编号:"+getCid()+",班级名称:"+getCname()+",班级学生:";}
}
【3】创建 ClassesMapper.java 接口,并添加 findClassesById 方法
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Classes;public interface ClassesMapper {Classes findClassesById(String id);
}
【4】创建 ClassesMapper.xml 映射文件,有以下两种方式:
方式一:多表连接查询方式。
这种方式只用到1条 SQL 语句,代码如下所示:
注意: 在
另外,在 “一方” 的映射文件中使用
方式二:多表单独查询方式。
多表连接查询方式是将多张表进行连接,连为一张表后进行查询。其查询的本质是一张表。而多表单独查询方式是多张表各自查询各自的相关内容,需要多张表的联合数据,再将主表的查询结果联合其它表的查询结果,封装为一个对象。
多个查询是可以跨越多个映射文件的,即是可以跨越多个namespace 的。在使用其它 namespace 的查询时,添加上其所在的 namespace 即可。这种方式要用到2条 SQL 语句,代码如下所示:
【5】创建 ClassesMapperTest 测试类,并添加如下方法:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Classes;
import cn.kgc.my01.entity.Student;
import junit.framework.TestCase;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;
import java.util.List;public class ClassesMapperTest{SqlSessionFactory factory=null;@Beforepublic void init(){try {System.out.println("########");InputStream resourceAsStream = Resources.getResourceAsStream("config/mybatis-config.xml");factory = new SqlSessionFactoryBuilder().build(resourceAsStream);} catch (IOException e) {e.printStackTrace();}}@Testpublic void testFindClassesById() {SqlSession sqlSession = factory.openSession(true);ClassesMapper mapper = sqlSession.getMapper(ClassesMapper.class);Classes classesById = mapper.findClassesById("201801");System.out.println(classesById.show());List students = classesById.getStudents();for (Student student : students) {System.out.println(student.show());}}
}
方式一:多表连接查询方式测试结果:
DEBUG [main] - ==> Preparing: select cid,cname,id,studentname,gender,age from classes,student where classes.cid=student.classno and classes.cid=?
DEBUG [main] - ==> Parameters: 201801(String)
DEBUG [main] - <== Total: 4
班级编号:201801,班级名称:计算机软件1班,班级学生:
学生编号:1,学生姓名:张飞,学生性别:男,学生年龄:18
学生编号:2,学生姓名:李白,学生性别:男,学生年龄:20
学生编号:3,学生姓名:张无忌,学生性别:男,学生年龄:19
学生编号:4,学生姓名:赵敏,学生性别:女,学生年龄:17
可以发现,只有一条 SQL 语句,并且是多表联查。
方式二:多表单独查询方式测试结果:
2023-02-15 10:56:49,965 [main] DEBUG DEBUG [main] - ==> Preparing: select cid,cname from classes where cid=?
DEBUG [main] - ==> Parameters: 201801(String)
DEBUG [main] - ====> Preparing: select * from student where classno=?
DEBUG [main] - ====> Parameters: 201801(String)
DEBUG [main] - <==== Total: 4
DEBUG [main] - <== Total: 1
班级编号:201801,班级名称:计算机软件1班,班级学生:
学生编号:1,学生姓名:张飞,学生性别:男,学生年龄:18
学生编号:2,学生姓名:李白,学生性别:男,学生年龄:20
学生编号:3,学生姓名:张无忌,学生性别:男,学生年龄:19
学生编号:4,学生姓名:赵敏,学生性别:女,学生年龄:17
可以发现,其 SQL 语句是两条,即各查各的,共用同一个参数。第 1 条先查一方的表,第 2 条再查多方的表。
多对一关联查询是指在查询多方对象的时候,同时将其所关联的一方对象也查询出来。
由于在查询多方对象时也是一个一个查询,所以多对一关联查询,其实就是一对一关联查询。即一对一关联查询的实现方式与多对一的实现方式是相同的。 配置多对一关联的重点在于“多方”的映射文件要有
项目案例: 查询学号为1的学生,同时获取他所在班级的完整信息
实现步骤:
【1】创建 StudentMapper.java 接口,并添加方法 searchStudentsById(int id) 如下:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Student;public interface StudentMapper {public Student searchStudentsById(int id);
}
【2】创建 StudentMapper.xml 映射文件,有以下两种方式:
方式一:多表联合查询。
方式二:多表单独查询。
【3】创建 StudentMapperTest 测试类,并添加如下方法:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Classes;
import cn.kgc.my01.entity.Student;
import junit.framework.TestCase;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;public class StudentMapperTest {SqlSessionFactory factory=null;@Beforepublic void init(){try {InputStream resourceAsStream = Resources.getResourceAsStream("config/mybatis-config.xml");factory = new SqlSessionFactoryBuilder().build(resourceAsStream);} catch (IOException e) {e.printStackTrace();}}@Testpublic void testSearchStudentsById() {SqlSession sqlSession = factory.openSession(true);StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);Student student = mapper.searchStudentsById(1);System.out.println(student.show());System.out.println("所在班级:");Classes classes=student.getClasses();System.out.println(classes.toString());}
}
方式一:多表联合查询方式测试结果:
DEBUG [main] - ==> Preparing: select cid,cname,id,studentname,gender,age from classes,student where classes.cid=student.classno and student.id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - <== Total: 1
学生编号:1,学生姓名:张飞,学生性别:男,学生年龄:18
所在班级:
Classes(cid=201801, cname=计算机软件1班, students=null)
可以发现,它发出的 SQL 语句是多表查询。
方式一:多表单独查询方式测试结果:
DEBUG [main] - ==> Preparing: select id,studentname,gender,age,classno from student where id=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: select cid,cname from classes where cid=?
DEBUG [main] - ====> Parameters: 201801(String)
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
学生编号:1,学生姓名:张飞,学生性别:男,学生年龄:18
所在班级:
Classes(cid=201801, cname=计算机软件1班, students=null)
可以发现,它发出的 SQL 语句是两条,即各查各的,共用同一个参数。
自连接的查询可以用一对多来处理,也可以用多对一来处理。例如,员工表,每个员工都有一个上司,但上司同时也是员工表的一条记录,这种情况可用自连接查询出每个员工对应的上司信息,也可以查出每个上司有哪些下属员工。
项目案例:查询员工的信息及对应的上司信息。
思路分析: 可将员工当做多方,上司当做一方。
实现步骤:
【1】修改数据库。
添加一个表 employee 并插入测试数据,具体如下:
create table employee (empid double ,empname varchar (60),job varchar (60),leader double
);
insert into employee (empid, empname, job, leader) values('1','jack','clerk','3');
insert into employee (empid, empname, job, leader) values('2','mike','salesman','3');
insert into employee (empid, empname, job, leader) values('3','john','manager','4');
insert into employee (empid, empname, job, leader) values('4','smith','president',NULL);
insert into employee (empid, empname, job, leader) values('5','rose','salesman','3');
【2】创建实体类 Employee,代码如下:
package cn.kgc.my01.entity;public class Employee {private int empid;private String empname;private String job;private Employee leader;public int getEmpid() {return empid;}public void setEmpid(int empid) {this.empid = empid;}public String getEmpname() {return empname;}public void setEmpname(String empname) {this.empname = empname;}public String getJob() {return job;}public void setJob(String job) {this.job = job;}public Employee getLeader() {return leader;}public void setLeader(Employee leader) {this.leader = leader;} public String toString(){return "员工编号:"+getEmpid()+",员工姓名:"+getEmpname()+",员工职位:"+getJob();}
}
可以发现,里面存在着嵌套,Employee 里面的一个属性 leader 本身就是 Employee 类型。
【3】创建 EmployeeMapper.java 接口,添加 findEmployeeAndLeaderById 方法如下:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Employee;public interface EmployeeMapper {Employee findEmployeeAndLeaderById(int id);
}
【4】创建 EmployeeMapper.xml 映射文件,代码如下:
【5】创建 EmployeeMapperTest.java 测试类
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Classes;
import cn.kgc.my01.entity.Employee;
import cn.kgc.my01.entity.Student;
import junit.framework.TestCase;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;
import java.util.Scanner;public class EmployeeMapperTest {SqlSessionFactory factory=null;@Beforepublic void init(){try {InputStream resourceAsStream = Resources.getResourceAsStream("config/mybatis-config.xml");factory = new SqlSessionFactoryBuilder().build(resourceAsStream);} catch (IOException e) {e.printStackTrace();}}@Testpublic void testFindEmployeeAndLeaderById() {SqlSession sqlSession = factory.openSession(true);EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee employee=mapper.findEmployeeAndLeaderById(1);Employee leader=employee.getLeader();System.out.println(employee.toString());System.out.println("他的上司是:"+leader.toString());//System.out.println("他的上司的上司是:"+leader.getLeader().toString());}
}
测试结果: 查询员工
DEBUG [main] - ==> Preparing: select * from employee where empid=?
DEBUG [main] - ==> Parameters: 1(Integer)
DEBUG [main] - ====> Preparing: select * from employee where empid=?
DEBUG [main] - ====> Parameters: 3(Integer)
DEBUG [main] - ======> Preparing: select * from employee where empid=?
DEBUG [main] - ======> Parameters: 4(Integer)
DEBUG [main] - <====== Total: 1
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
员工编号:1,员工姓名:jack,员工职位:clerk
他的上司是:员工编号:3,员工姓名:john,员工职位:manager
从上面的 SQL 语句中发现,出现了 3 条 SQL 语句,这个查询存在嵌套,先查员工1,然后查他的直接上司3,再查上司的上司4。这种情况不影响什么,甚至可以实现直接输出上司的上司,但要注意输出语句不要出现地柜,即输出语句不要出现输出上司。
要同时查上司的上司,只需要在上面的测试类中多加一条语句:
System.out.println("他的上司的上司是:"+leader.getLeader().toString());
项目案例:查询某位领导及其直接下属员工。
思路分析: 可用一对多的方式来实现,员工(领导)当作一方,员工(下属)当作多方。
实现步骤:
【1】修改实体类 Employee,代码如下:
package cn.kgc.my01.entity;import java.util.List;public class Employee {private int empid;private String empname;private String job;//员工的上司private Employee leader;//员工的下属private List employees;public List getEmployees() {return employees;}public void setEmployees(List employees) {this.employees = employees;}public int getEmpid() {return empid;}public void setEmpid(int empid) {this.empid = empid;}public String getEmpname() {return empname;}public void setEmpname(String empname) {this.empname = empname;}public String getJob() {return job;}public void setJob(String job) {this.job = job;}public Employee getLeader() {return leader;}public void setLeader(Employee leader) {this.leader = leader;}public String toString(){return "员工编号:"+getEmpid()+",员工姓名:"+getEmpname()+",员工职位:"+getJob();}
}
【2】在 EmployeeMapper.java 接口中,添加 findLeaderAndEmployeesById 方法如下:
Employee findLeaderAndEmployeesById(int id);
【3】在 EmployeeMapper.xml 映射文件中,添加 findEmployeeAndLeaderById 的映射方法内容如下:
【4】在 EmployeeMapperTest.java 测试类中,添加如下内容:
@Testpublic void testTestFindLeaderAndEmployeesById() {SqlSession sqlSession = factory.openSession(true);EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);Employee leader=mapper.findLeaderAndEmployeesById(4);List employees = leader.getEmployees();System.out.println(leader.toString());System.out.println("他的直接下属有:");for (Employee employee : employees) {System.out.println(employee.toString());}}
测试结果: 查询经理
DEBUG [main] - ==> Preparing: select * from employee where empid=?
DEBUG [main] - ==> Parameters: 4(Integer)
DEBUG [main] - ====> Preparing: select * from employee where leader=?
DEBUG [main] - ====> Parameters: 4.0(Double)
DEBUG [main] - <==== Total: 1
DEBUG [main] - <== Total: 1
员工编号:4,员工姓名:smith,员工职位:president
他的直接下属有:
员工编号:3,员工姓名:john,员工职位:manager
原理: 多对多可以分拆成两个一对多来处理,需要一个中间表,各自与中间表实现一对多的关系。
项目案例:一个学生可以选人修多门课程,一门课程可以给多个学生选修,课程与学生之间是典型的多对多。实现查询一个学生信息,同时查出他的所有选修课,还有实现查询一门课程信息,同时查出所有的选修了该课程的学生信息。
思路分析: 多对多需要第三表来体现,数据库中除了课程表,学生表,还需要学生课程表。
实现步骤:
【1】修改数据库,代码如下:
CREATE DATABASE studentdb;
USE studentdb ;
DROP TABLE IF EXISTS student ;
CREATE TABLE student (id INT(11) NOT NULL,studentname VARCHAR(20) DEFAULT NULL,gender CHAR(2) DEFAULT NULL,age INT(11) DEFAULT NULL,classno VARCHAR(10),PRIMARY KEY ( id )
)
INSERT INTO student ( id , studentname , gender , age , classno ) VALUES (1,'张飞','男',18,'201801'),(2,'李白','男',20,'201801'),(3,'张无忌','男',19,'201801'),(4,'赵敏','女',17,'201801');CREATE TABLE classes (cid VARCHAR (30),cname VARCHAR (60)
);
INSERT INTO classes (cid, cname) VALUES('201801','计算机软件1班');
INSERT INTO classes (cid, cname) VALUES('201802','计算机软件2班');CREATE TABLE employee (empid DOUBLE ,empname VARCHAR (60),job VARCHAR (60),leader DOUBLE
);
INSERT INTO employee (empid, empname, job, leader) VALUES('1','jack','clerk','3');
INSERT INTO employee (empid, empname, job, leader) VALUES('2','mike','salesman','3');
INSERT INTO employee (empid, empname, job, leader) VALUES('3','john','manager','4');
INSERT INTO employee (empid, empname, job, leader) VALUES('4','smith','president',NULL);
INSERT INTO employee (empid, empname, job, leader) VALUES('5','rose','salesman','3');CREATE TABLE course (courseid DOUBLE ,coursename VARCHAR (90)
);
INSERT INTO course (courseid, coursename) VALUES('1','java');
INSERT INTO course (courseid, coursename) VALUES('2','android');
INSERT INTO course (courseid, coursename) VALUES('3','PHP');CREATE TABLE studentcourse (id DOUBLE ,studentid DOUBLE ,courseid DOUBLE
);
INSERT INTO studentcourse (id, studentid, courseid) VALUES('1','1','1');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('2','1','2');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('3','2','1');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('4','2','2');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('5','3','1');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('6','3','2');
INSERT INTO studentcourse (id, studentid, courseid) VALUES('7','1','3');
【2】新增实体类 Course 和修改实体类 Student 。
Course 类如下:
package cn.kgc.my01.entity;import lombok.Data;import java.util.List;@Data
public class Course {private int courseid;private String coursename;private List students;public String toString(){return "课程编号:"+getCourseid()+",课程名称:"+getCoursename();}
}
Student类如下,添加一个属性courses和getter,setter方法。
private List courses;public List getCourses() {return courses;}public void setCourses(List courses) {this.courses = courses;}
【3】新建 StudentMapper.java 接口,并添加一个方法如下:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Student;public interface StudentMapper {public Student searchStudentById(int id);
}
【4】配置对应的 StudentMapper.xml 映射,代码如下:
【5】创建测试类 StudentMapperTest 类
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Classes;
import cn.kgc.my01.entity.Course;
import cn.kgc.my01.entity.Student;
import junit.framework.TestCase;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;
import java.util.List;public class StudentMapperTest {SqlSessionFactory factory=null;@Beforepublic void init(){try {InputStream resourceAsStream = Resources.getResourceAsStream("config/mybatis-config.xml");factory = new SqlSessionFactoryBuilder().build(resourceAsStream);} catch (IOException e) {e.printStackTrace();}}@Testpublic void testSearchStudentsById() {SqlSession sqlSession = factory.openSession(true);StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);Student student = mapper.searchStudentById(1);System.out.println(student.show());System.out.println("-----该生选修了以下课程:-----------");List courses=student.getCourses();for(Course course:courses){System.out.println(course.toString());}}}
测试结果:
【6】新建 CourseMapper.java 接口,并添加一个方法如下:
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Course;public interface CourseMapper {//根据id查找课程,即时获取选个性该课程的学生public Course searchCourseById(int id);
}
【7】配置对应的 CourseMapper.xml 映射,代码如下:
【8】创建测试类 CourseMapperTest 类
package cn.kgc.my01.mapper;import cn.kgc.my01.entity.Course;
import cn.kgc.my01.entity.Employee;
import cn.kgc.my01.entity.Student;
import junit.framework.TestCase;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.junit.Before;
import org.junit.Test;import java.io.IOException;
import java.io.InputStream;
import java.util.List;public class CourseMapperTest{SqlSessionFactory factory=null;@Beforepublic void init(){try {InputStream resourceAsStream = Resources.getResourceAsStream("config/mybatis-config.xml");factory = new SqlSessionFactoryBuilder().build(resourceAsStream);} catch (IOException e) {e.printStackTrace();}}@Testpublic void testSearchCourseById() {SqlSession sqlSession = factory.openSession(true);CourseMapper mapper = sqlSession.getMapper(CourseMapper.class);Course course = mapper.searchCourseById(1);System.out.println(course.toString());System.out.println("-------该课程有以下学生选修:------");List students=course.getStudents();for(Student student:students){System.out.println(student.show());}}
}
测试效果:
码文不易,本篇文章就介绍到这里,如果想要学习更多Java系列知识,点击关注博主,博主带你零基础学习Java知识。与此同时,对于日常生活有困扰的朋友,欢迎阅读我的第四栏目:《国学周更—心性养成之路》,学习技术的同时,我们也注重了心性的养成。