1、N+1问题
N+1问题主要是针对分步查询,分步查询就是使用association或collection标签中的select属性来执行另外一个 SQL 映射语句来返回预期的复杂类型,例如:
<association property="department" column="department_id" javaType="department" select="com.thr.mapper.DepartmentMapper.selectDeptByDeptId"/>
在前面的代码中(分步查询),我们所有的级联都已经成功了,但是这样会引发性能问题,就是我们查询数据时,级联的数据也会跟着全部查询出来。但是如果我们暂时只需要部门的信息,而不需要级联对象中的信息,这就会使数据库多执行几条毫无意义的SQL,导致数据库资源的损耗和系统性能的下降。而如果有多重级联的话则会更加明显,假如现在有N个级联,本来我们只要查询主数据,只要一次查询就可以了,但是由于级联的关系,其级联的数据也会跟着查询出来,后面的SQL语句还会执行N次,这样就造成了N+1问题。
以前面员工和部门的例子举例:我们本来要的是部门数据,根据部门id查询部门信息,但是由于一个部门可以有多个员工,级联数据会跟着查询出来,假如现在有N个员工,那么查询的语句如下:
根据部门id查询 SELECT * FROM t_department WHERE department_id = #{id} 级联的查询(分步查询): SELECT * FROM t_employee where department_id= 1 SELECT * FROM t_employee where department_id= 2 SELECT * FROM t_employee where department_id= 3 ...... SELECT * FROM t_employee where department_id= n-1 SELECT * FROM t_employee where department_id= n
本来我们只需要部门的一条数据即可,但是查询了N+1次,这是不合理的。那么为了应对N+1问题,Mybatis提供了延迟加载功能。
2、什么是延迟加载
延迟加载也叫做懒加载、惰性加载。就是我们希望一次性把常用的级联数据通过SQL直接查询出来,对于那些不想要的级联数据则不要取出,而是等待要用的时候才取出来。在mybatis中,resultMap可以实现高级映射(使用association、collection实现一对一及一对多映射),association、collection具备延迟加载功能。
3、如何开启延迟加载
开启延迟加载有两种方式:
①、全局配置延迟加载:这种方式就是给所有的级联配置延迟加载。Mybatis默认是不开启延迟加载的,需要我们去全局配置文件中打开延迟加载。
配置项 | 作用 | 配置值 | 默认值 |
lazyLoadingEnabled | 全局的延迟加载开关,开启时,所有级联对象都会延迟加载 | true | false | false |
aggressiveLazyLoading | 当启用时,有延迟加载属性的对象在被调用时将会加载该对象的所有属性。 否则,每个属性会按需加载 | true | false | false 版本3.4.1之前为true |
注意:这个aggressiveLazyLoading有点不好理解,下面会有介绍。
全局配置如下:
<settings> <!-- 开启延迟加载,不配置默认关闭该特性--> <setting name="lazyLoadingEnabled" value="true"></setting> <setting name="aggressiveLazyLoading" value="false"/> </settings>
②、fetchType属性为局部配置延迟加载:这种方式需要用到fetchType属性,主要解决全局配置的缺点,因为不是所有的地方都要使用到延迟加载,fetchType出现在级联元素association和collection中,它存在两个值:
- eager:立即加载对应的数据。
- lazy:延迟加载对应的数据。
4、全局配置实现延迟加载
以前面部门Department和员工Employee为例,一个部门可以包含多名员工的一对多关系。
①、创建员工和部门实体类:
员工实体类:
/** * 员工实体类 */ public class Employee { //员工id private Integer empId; //员工名称 private String empName; //员工年龄 private Integer empAge; //员工性别 private Integer empSex; //员工邮箱 private String empEmail; //员工地址 private String empAddress; //员工所属部门,和部门表构成一对一的关系,一个员工只能在一个部门 private Department department; //getter、setter、toString方法和一些构造方法省略... }
部门实体类:
/** * 部门实体类 */ public class Department { //部门id private Integer deptId; //部门名称 private String deptName; //部门有哪些员工,一对多关系 private List<Employee> employees; //getter、setter、toString方法和一些构造方法省略... }
②、创建员工和部门Mapper接口
EmployeeMapper:
/** * 员工Mapper接口 */ public interface EmployeeMapper { //据据员工表的department_id查询员工数据,用于一对多的关联查询 Employee selectEmpByDeptId(@Param("id") Integer deptId); }
DepartmentMapper:
/** * 部门Mapper接口 */ public interface DepartmentMapper { //查询所有数据 List<Department> selectAll(); //根据部门id查询数据 Department selectDeptByDeptId(@Param("id") Integer deptId); }
③、创建员工和部门SQL映射文件
EmployeeMapper.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.thr.mapper.EmployeeMapper"> <resultMap id="employeeMap" type="com.thr.pojo.Employee"> <id property="empId" column="employee_id"/> <result property="empName" column="employee_name"/> <result property="empAge" column="employee_age"/> <result property="empSex" column="employee_sex"/> <result property="empEmail" column="employee_email"/> <result property="empAddress" column="employee_address"/> <!-- 一对一关联对象--> <!--<association property="department" column="department_id" javaType="department" select="com.thr.mapper.DepartmentMapper.selectDeptByDeptId"/>--> </resultMap> <!--根据员工表的department_id查询员工数据,用于一对多的关联查询--> <select id="selectEmpByDeptId" parameterType="int" resultMap="employeeMap"> SELECT * FROM t_employee where department_id= #{id} </select> </mapper>
DepartmentMapper.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.thr.mapper.DepartmentMapper"> <resultMap id="departmentMap" type="com.thr.pojo.Department"> <id property="deptId" column="department_id"/> <result property="deptName" column="department_name"/> <!--一对多关联对象,ofType指定的是映射到list集合属性中pojo的类型,也就是尖括号的泛型 注意:这里的column属性首先是查询出t_department表的department_id,然后将它以参数的形式传递给select属性 中的EmployeeMapper.selectEmpByDeptId方法,进而查询出当前部门下的员工--> <collection property="employees" ofType="employee" column="department_id" select="com.thr.mapper.EmployeeMapper.selectEmpByDeptId"> </collection> </resultMap> <!-- 查询所有数据--> <select id="selectAll" resultMap="departmentMap"> SELECT * FROM t_department </select> <!--根据部门id查询数据--> <select id="selectDeptByDeptId" parameterType="int" resultMap="departmentMap"> SELECT * FROM t_department WHERE department_id = #{id} </select> </mapper>
④、在Mybatis的配置文件中开启全局延迟加载
<?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>
<!--引入properties文件-->
<properties resource="db.properties"/>
<!-- 全局配置参数,需要时再设置 -->
<settings>
<!-- 打开延迟加载的开关 -->
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="aggressiveLazyLoading" value="false"/>
</settings>
<!--配置别名-->
<typeAliases>
<!-- 对包进行扫描,可以批量进行别名设置,设置规则是:获取类名称,将其第一个字母变为小写 -->
<package name="com.thr.pojo"/>
</typeAliases>
<!-- 配置环境-->
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"></transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${database.driver}"/>
<property name="url" value="${database.url}"/>
<property name="username" value="${database.username}"/>
<property name="password" value="${database.password}"/>
</dataSource>
</environment>
</environments>
<!--注册mapper,通过扫描的方式-->
<mappers>
<package name="com.thr.mapper"/>
</mappers>
</configuration>
⑤、数据库连接和日志文件
db.properties:
#数据库连接配置 database.driver=com.mysql.cj.jdbc.Driver database.url=jdbc:mysql://localhost:3306/mybatis?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8 database.username=root database.password=root
log4j.properties:
log4j.rootLogger=DEBUG, Console #Console log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n log4j.logger.java.sql.ResultSet=INFO log4j.logger.org.apache=INFO log4j.logger.java.sql.Connection=DEBUG log4j.logger.java.sql.Statement=DEBUG log4j.logger.java.sql.PreparedStatement=DEBUG
⑥、测试代码
/** * 测试代码 */ public class MybatisTest { //定义 SqlSession private SqlSession sqlSession = null; //定义 DepartmentMapper对象 private DepartmentMapper mapper = null; @Before//在测试方法执行之前执行 public void getSqlSession(){ //1、加载 mybatis 全局配置文件 InputStream is = MybatisTest.class.getClassLoader().getResourceAsStream("mybatis-config.xml"); //2、创建SqlSessionFactory对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is); //3、根据 sqlSessionFactory 产生 session sqlSession = sqlSessionFactory.openSession(); //4、创建Mapper接口的的代理对象,getMapper方法底层会通过动态代理生成UserMapper的代理实现类 mapper = sqlSession.getMapper(DepartmentMapper.class); } @After//在测试方法执行完成之后执行 public void destroy() throws IOException { sqlSession.commit(); sqlSession.close(); } //查询所有数据 @Test public void testSelectAll(){ List<Department> departments = mapper.selectAll(); for (Department department : departments) { System.out.println(department.getDeptId()+"--"+department.getDeptName()); System.out.println("========"); //执行getEmployees()去查询员工信息,这里实现按需加载 //System.out.println(department.getEmployees()); } } //根据部门id查询数据 @Test public void testSelectDeptByDeptId(){ Department department = mapper.selectDeptByDeptId(3); System.out.println(department.getDeptId()+"--"+department.getDeptName()); System.out.println("========"); //执行getEmployees()去查询员工信息,这里实现按需加载 //System.out.println(department.getEmployees()); } }
⑦、测试结果
(1)、只使用部门数据,这时候不需要查询员工信息
可以发现只查询了部门的信息。
(2)、需要使用员工数据,这时候因为需要获取员工数据所以要查询员工信息。
我们发现部门和员工数据都查询了,先是查询了部门的信息,然后我们又要获取员工的信息,所有后面再去查询员工的信息。
而我们在关闭延迟加载的查询时这样的,它直接给我们的数据全部都查询出来了,而不是等到我想要的时候才去查询。
5、fecthLazy实现局部延迟加载
fecthLazy实现局部延迟加载的方式配置非常简单,如下:
①、我们把Mybatis的全局配置文件中的开启延迟加载的配置删除或者注释
②、然后将我们需要设置为延迟加载的地方设置fecthLazy=lazy即可
③、执行的结果和前面的测试结果是一样的,所以我们一般推荐使用这种方式,这种方式的好处就是我哪里想要延迟加载就设置哪里即可。
6、lazyLoadingEnabled和aggressiveLazyLoading的使用
lazyLoadingEnabled:主要控制延迟加载的开关。为true时表示开启延迟加载,false关闭。
aggressiveLazyLoading:true表示如果有延迟加载属性的对象在被调用时将会加载该对象的所有属性,false则表示每个属性会按需加载。
我们主要来学习一下aggressiveLazyLoading属性
(1)、aggressiveLazyLoading的属性为false,即每种属性按需加载,不调用就不加载。
运行测试方法:
运行结果:
可以看到只执行department的sql语句,并且只加载了调用的属性。
(2)、aggressiveLazyLoading设置为true:只要对这个类的任意操作将完整加载整个类的所有属性,即执行级联的SQL语句。运行同样的测试方法:
我们只调用了department对象中的getXxx方法,而并没有调用employee对象中的getXxx方法,但是mybatis却调用了查询员工SQL语句。
这下对这个属性有所了解了吧,如果没有看懂的可以去参考:https://blog.csdn.net/qq_42650817/article/details/103262158