- 多对一关系
- 1、保存员工、部门信息
- 2、根据id查询员工信息
- 3、额外SQL的N+1问题
- 4、内联映射(处理结果集和对象属性的不匹配)
- 5、总结 (多对一配置)
- 一对多关系
- 1、保存员工、部门信息
- 2、根据id查询员工信息
- 3、内联映射(处理结果集和对象属性的不匹配)
- 延迟加载
- 关联对象的配置选择
- 多对多关系
- 1、保存学生、老师、中间表的信息
- 2、删除指定id的学生
- 3、查询指定id的学生(包括该id学生的老师)
MyBatis源码及资料: https://github.com/coderZYGui/MyBatis-Study
MyBatis系列
- MyBatis — ORM思想、MyBatis概述、日志框架、OGNL
- MyBaits — MyBatis的CRUD操作、别名配置、属性配置、查询结果映射、Mapper组件、参数处理、注解开发
- MyBatis — 动态SQL、if、where、set、foreach、sql片段
- MyBatis — 对象关系映射、延迟加载、关联对象的配置选择
- MyBatis — 缓存机制、EhCache第三方缓存
- MyBatis — MyBatis Generator插件使用(配置详解)
方便了解 关联关系的各种关系,请先参考: UML一一 类图关系 (泛化、实现、依赖、关联、聚合、组合)
下面的体现的都是关联关系
跳转到目录
案例: 多个员工对象属于同一个部门对象
- 1、保存员工部门信息
- 2、根据id来查询员工信息
- 3、查询所有用户
CREATE TABLE `employee1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`dept_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8;
CREATE TABLE `department1` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=43 DEFAULT CHARSET=utf8;
模型对象设计
Employee类 Department类
跳转到目录
MapperEmployeeMapper
// 保存员工信息
void save(Employee emp);
INSERT INTO employee1(name, dept_id) VALUES (#{name}, #{dept.id})
DepartmentMapper
// 保存一个部门
void save(Department dept);
INSERT INTO department1(name) VALUES (#{name})
测试类
/**
* 测试保存操作
*/
@Test
public void test() {
// 创建部门对象
Department dept = new Department();
dept.setName("开发部");
// 创建员工对象
Employee emp = new Employee();
emp.setName("张三");
emp.setDept(dept); // 将张三分配到开发部中
Employee emp2 = new Employee();
emp2.setName("李四");
emp2.setDept(dept); // 将李四分配到开发部中
SqlSession sqlSession = MybatisUtils.getSqlSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
// 保存操作:先保存部门(one方)因为,员工表中需要依赖于部门ID,先保存员工信息,部门信息可能为空
departmentMapper.save(dept);
employeeMapper.save(emp);
employeeMapper.save(emp2);
sqlSession.commit();
sqlSession.close();
}
跳转到目录
通过额外SQL
EmployeeMapper
// 根据id来查询员工信息
Employee getUserById(long empId);
SELECT id, name, dept_id FROM employee1 WHERE id = #{id}
DepartmentMapper
// 根据部门的id查询出部门对象
Department getDeptById(Long deptId);
SELECT id, name FROM department1 WHERE id = #{id}
测试类
因为获取的结果集中的列名和对象的属性名不匹配,设置resultMap
解决不匹配问题, 但是结果集中的dept_id列
,不能和对象中的dept对象
匹配,只能和对象dept的属性id匹配 所以,能获取到部门的id,但是期望得到的是Department对象
解决方案:
根据部门的id
,再 额外
发送一条SQL语句,查询出 部门对象
,把部门对象设置给员工
即可!
/**
* 根据id来查询员工信息
*/
@Test
public void test1() {
SqlSession sqlSession = MybatisUtils.getSqlSession();
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
Employee employee = employeeMapper.getUserById(2L);
// -------------------------------------
/*
因为获取的结果集中的列名和对象的属性名不匹配,设置resultMap解决不匹配问题,
但是结果集中的id列,不能和对象中的dept对象匹配,只能和对象dept的属性id匹配
所以,能获取到部门的id,但是期望得到的是Department对象
解决方案: 根据部门的id,再 额外 发送一条SQL语句,查询出 部门对象 ,把部门对象设置给员工即可!
*/
// 下面三行代码让,MyBatis完成,需要在resultMap中配置
Long deptId = employee.getDept().getId();
// 根据部门id来查询出,部门对象
Department department = departmentMapper.getDeptById(deptId);
employee.setDept(department);
System.out.println(employee);
System.out.println(employee.getDept());
}
在测试方法中
不想自己手动发额外的SQL
怎么办呢?MyBatis可以帮我们完成,这就需要在resultMap
中配置一下
额外SQL的配置方式
- association元素: 配置单一元素(非数组、集合)的关联对象
property
属性: 关联对象的属性名; 这里跟的就是Employee中的属性名称javaType
属性: 关联对象属性类型select
属性: 发送额外的SQLcolumn
属性: 指定列的值,传递给SQL语句
- 这样以来,就不用了在测试方法中不用自己去写了,实际操作是一样的,只是把额外发送的SQL封装配置起来了.
- 在
DepartmentMapper
中就不需要写Department getDeptById(Long deptId)
接口方法了
跳转到目录 假如每一个员工的部门ID都是不同的, 查询所有员工信息, 此时就会发生N+1
问题, 其实就是发送了N+1
条SQL语句
1
: SELECT * FROM employeeN
: SELECT * FROM department WHERE id = ?
当查询所有员工时,如下图: (只要是不同部门的员工, 都会额外会根据dept_id去查一下部门信息) 解决方案: 使用
多表查询(JSON查询)
, 一条SQL语句就搞定
跳转到目录
EmployeeMapper
//查询所有用户信息(包括部门)
List getAllUser();
SELECT
e.id,
e.name,
d.id AS d_id,
d.name AS d_name
FROM
employee1 e JOIN department1 d
ON e.dept_id = d.id
这样一来, 通过关联查询, id, name, d_id, d_name就和上面设置的对应上了
DepartmentMapper 不需要写
这样一来, 当查所有的用户信息时, 就会把每个用户的部门信息也查出来,并赋值给用户的dept属性
测试类/**
* 查询所有用户
*/
@Test
public void test2(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
EmployeeMapper mapper = sqlSession.getMapper(EmployeeMapper.class);
List emps = mapper.getAllUser();
for (Employee emp : emps) {
System.out.println(emp);
}
sqlSession.close();
}
跳转到目录
- 使用
association元素
,配置单一对象属性- 方式一:
额外SQL
(分布查询), 一般,需要进入另一个页面显示更详细信息的时候.–>集合属性 - 方式二: 内联映射 (多表查询)
- 方式一:
如果需要在查询出来的信息包含关联对象
(这就是上面的在Employee类中,关联的属性–> Department dept)的数据, 就需要使用 内联映射
, 否则就会出现N+1问题;
总结:
多对一问题:
1、主要是在employee中添加了Department dept属性,
所以就会导致employee表中的字段和employee类中的属性, 出现不一致, 因为表中的字段为`dept_id`, 而类中为dept属性(是一个部门对象),
因为我们可以拿到`dept_id`, 所以, 我们先通过dept_id额外去部门mapper中
查询到部门对象, 然后将部门对象赋值给employee.dept;
2、发送额外SQL, 可以去手动编写(看上面代码), 也可以通过标签中的
它就可以用来发额外SQL;
3、这样虽然可以了, 但是又发生一个问题, 如果同时查询多个员工的部门, 假如每个员工的部门都不同,
此时`查询所有员工, 都需要额外的根据每个员工表中的dept_id去查询部门对象(发SQL)`, 这样SQL就太多;
也就是 N+1 条SQL问题
4、通过内联查询来解决N+1, 也就是多表直接的关联查询,
SELECT e.id, e.name, d.id, d.name FROM employee e JOIN department d ON e.dept_id = d.id
一对多(关系在一的一方)
跳转到目录
- 案例: 多个员工对象属于同一个部门对象
- 1、保存员工部门信息
- 2、查询10号ID的部门信息
CREATE TABLE `employee2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
`dept_id` bigint(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
CREATE TABLE `department2` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8;
模型对象设计
Employee类 Department类
跳转到目录
MapperEmployeeMapper
// 保存员工信息
void save(Employee emp);
INSERT INTO employee2(name, dept_id) VALUES (#{name}, #{deptId})
DepartmentMapper
// 保存一个部门
void save(Department dept);
INSERT INTO department2(name) VALUES (#{name})
测试类
/**
* 测试保存操作
*/
@Test
public void test() {
// 创建部门对象
Department dept = new Department();
dept.setName("开发部");
// 创建员工对象
Employee emp = new Employee();
emp.setName("张三");
Employee emp2 = new Employee();
emp2.setName("李四");
// 维护对象关系
dept.getEmps().add(emp);
dept.getEmps().add(emp2);
SqlSession sqlSession = MybatisUtils.getSqlSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
departmentMapper.save(dept);
// 当保存部门信息后,再获取部门ID
emp.setDeptId(dept.getId());
emp2.setDeptId(dept.getId());
employeeMapper.save(emp);
employeeMapper.save(emp2);
sqlSession.commit();
sqlSession.close();
}
跳转到目录
通过额外SQL
EmployeeMapper
// 根据部门id来查询部门的员工信息
List getUsersByDeptId(Long deptId);
SELECT id, name, dept_id AS deptId FROM employee2 WHERE dept_id = #{deptId}
DepartmentMapper
SELECT id, name FROM department2 WHERE id = #{id}
注意: 这里关联属性是集合类型
的,所以要使用collection
元素
/**
* 查询10号ID的部门信息(包括部门的员工)
*/
@Test
public void test2(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
DepartmentMapper departmentMapper = sqlSession.getMapper(DepartmentMapper.class);
Department dept = departmentMapper.getDeptById(10L);
System.out.println(dept);
System.out.println(dept.getEmps());
/*
* 手动完成 额外SQL查询
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
Long deptId = dept.getId();
List emps = employeeMapper.getUsersByDeptId(deptId);
dept.setEmps(emps);
List emps1 = dept.getEmps();
System.out.println(emps1);
*/
sqlSession.close();
}
跳转到目录
DepartmentMapper
// 根据id查询部门信息
Department getDeptById(long l);
SELECT d.id, d.name,
e.id AS e_id, e.name AS e_name
FROM department2 d JOIN employee2 e ON d.id = e.dept_id
WHERE d.id = #{id}
EmployeeMapper 不需要写
和上面相同
跳转到目录
延迟加载(lazy load) : 也称为懒加载
。
- 为了避免一些无谓的性能开销而提出来的,所谓延迟加载就是
当在真正需要数据的时候,才会发出sql语询进行查询该数据
。 - MyBatis运行时期的属性配置,放在主配置文件中的
元素中。
- MyBatis中直接查询出来的many方对象其实就已经是一个代理对象,当使用many方对象的任意一个属性时,立刻很积极的把关联对象也查询出来,此时性能不会太好。
- 配置细节: ①MyBatis
缺省情况下,禁用了延迟加载
。 ②MyBatis 会很积极的去查询关联对象。 ③MyBatis 中缺省情况下调用equals. clone. hashCode、 toString 都会触发延迟加载 一般的我们保留clone就可以了,也就是说调用many方对象的toString. hashCode. equals 方法依然不会去发送查询one方的SQL.
在MaBatis的主配置文件中
这样以来就不会发送额外SQL
了
跳转到目录
- 针对
单属性
对象, 使用association
元素,通常直接使用多表查询
操作,也就是使用内联查询
- 针对
集合属性
对象,使用collection
元素,通常使用延迟加载
, 也就是额外SQL
处理
跳转到目录
- 1、保存学生、老师、中间表的信息
- 2、删除指定id的学生
- 3、查询指定id的学生(包括该id学生的老师)
CREATE TABLE `student` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `teacher` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `student_teacher` (
`student_id` bigint(20) NOT NULL,
`teacher_id` bigint(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
模型对象设计
Student类 Teacher类
跳转到目录
MapperStudentMapper
// 保存学生信息
void save(Student stu);
// 因为关系在学生这边.(因为传递的是两个参数,所以要做参数处理,使用注解)
void insertRelationWithTeacher(@Param("studentId") Long studentId, @Param("teacherId") Long teacherId);
INSERT INTO student (name) VALUES (#{name})
INSERT INTO student_teacher (student_id, teacher_id) VALUES (#{studentId}, #{teacherId})
TeacherMapper
// 保存老师信息
void save(Teacher teacher);
INSERT INTO teacher(name) VALUES (#{name})
测试类
/**
* 将学生、老师信息存储到各自的表中
* 并将学生、老师的ID插入到中间表中
*/
@Test
public void testSave() {
Teacher t1 = new Teacher();
t1.setName("老师1");
Teacher t2 = new Teacher();
t2.setName("老师2");
Student s1 = new Student();
s1.setName("张三");
Student s2 = new Student();
s2.setName("李四");
// 维护关系(学生的老师们)
s1.getTeachers().add(t1);
s1.getTeachers().add(t2);
s2.getTeachers().add(t1);
s2.getTeachers().add(t2);
SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
TeacherMapper teacherMapper = sqlSession.getMapper(TeacherMapper.class);
studentMapper.save(s1);
studentMapper.save(s2);
teacherMapper.save(t1);
teacherMapper.save(t2);
// 当学生和老师信息都保存之后,再维护中间表的数据
for (Teacher teacher1 : s1.getTeachers()) {
studentMapper.insertRelationWithTeacher(s1.getId(), teacher1.getId());
}
for (Teacher teacher2 : s2.getTeachers()) {
studentMapper.insertRelationWithTeacher(s2.getId(), teacher2.getId());
}
sqlSession.commit();
sqlSession.close();
}
跳转到目录
- 当删除指定
id
的学生前, 要删除中间表中 关于该id的学生, 以及其教师
的关联关系
StudentMapper
// 根据id删除学生记录
void deleteStuById(long l);
// 当删除学生的id时,中间表中学生的id也应该删除
void deleteRelationTeacherById(long studentId);
DELETE FROM student_teacher WHERE student_id = #{studentId}
DELETE FROM student WHERE id = #{id}
测试类
/**
* 根据ID删除
*/
@Test
public void test(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
// 因为多对多关系, 中间表中列,是两表中的主键,当删除一个学生时,中间表也必然发生改变
// 在删除学生之前,应该先删除中间表中的数据; 否则会引发外键约束
studentMapper.deleteRelationTeacherById(1L);
studentMapper.deleteStuById(1L);
sqlSession.commit();
sqlSession.close();
}
跳转到目录 使用额外SQL
因为 额外SQL(分布查询), 一般,需要进入另一个页面显示更详细信息的时候.–>集合属性
StudentMapper
// 根据id来查询学生信息
Student get(Long l);
SELECT id,name FROM student WHERE id = #{id}
TeacherMapper
SELECT t.id, t.name
FROM teacher t JOIN student_teacher st on t.id = st.teacher_id
WHERE st.student_id = #{studentId}
测试类
/**
* 根据Id查询学生信息(包括该学生的老师们)
*/
@Test
public void testQueryByStuId(){
SqlSession sqlSession = MybatisUtils.getSqlSession();
StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
Student student = studentMapper.get(1L);
System.out.println(student);
System.out.println(student.getTeachers());
}
2021.4.21补充
select id,dept_name from tbl_dept where id=#{id}