1. 多表查询
前面的操作(MyBatis进阶),不管是用 SqlSession还是 Mapper代理,都是对单个数据库表的操作
在实际开发中,经常会将来自多张表的数据在一个位置显示,比如查询并显示员工信息时,需要展示部门表的部门名称,岗位表的岗位名称等。这就要求 Employee中,要包含部门 Dept、岗位 Position的信息
MyBatis是如何实现对多表的查询并组装数据呢?(此时没有DML的事)
- 方式1:无级联查询——开发者手动完成多表数据的组装,需要执行多条 SQL语句
- 方式2:级联查询——使用MyBatis映射配置自动完成数据的组装,需要执行多条 SQL语句
- 方式3:
连接查询
——使用MyBatis映射配置自动完成数据的组装,只执行一条 SQL语句
以 部门Dept 和 员工Emp 的一对多关系为例:
-
功能1:查询所有员工的信息(多对一:多个员工对应一个部门)
包含字段:empno、ename、sal、comm、deptno、dname
其中 deptno是关联字段,dname来自部门表Dept
-
功能2:查询10号部门及其该部门员工信息(一对多:一个部门对应多个员工)
包含字段:deptno、dname、loc。 员工显示empno、ename、sal、comm
1.1 环境搭建
需要定义两个实体类、两个映射接口、两个映射XML文件(注意在mybatis-cfg.xml中告诉映射文件的位置,可以使用package)
需要在两个实体类之间建立关联关系,数据库之间通过外键建立关联,类之间通过属性
建立关联(使用组合)
-
一个员工只有一个部门,所以在 Employee中,定义一个 Dept类的成员变量
public class Employee { private Dept dept; // 虽然 Dept里包含了 deptno,但是在 Employee中的也不能省略,供 insert和 update使用 // 其他属性省略,均有 getter/setter方法 }
-
一个部门有多个员工,所以在 Dept中,定义一个泛型为 Employee的集合的成员变量
public class Dept { private List<Employee> employeeList; // 其他属性省略,均有 getter/setter方法 }
1.2 无级联查询(不使用,了解)
需要开发者手动的进行多次查询并完成多表数据的组装,需要执行多个 SQL语句,很少使用
实现功能1(多对一):
-
测试类中:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class); DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class); List<Employee> employeeList = empMapper.findAll(); // 在代码中进行组装!!!!!!!!!!!! for (Employee employee : employeeList) { // 获取员工的部门编号 int deptno = employee.getDeptno(); // 按照部门编号查找部门数据 Dept dept = deptMapper.findById(deptno); // 将数据赋给Employee的dept字段 employee.setDept(dept); } DBUtil.closeSqlSession(sqlSession); employeeList.forEach(emp -> { if (emp.getDept() == null) { System.out.println(emp.getEmpno() + "\t" + emp.getEname() + "\t" + emp.getSal() + "\t 无部门"); } else { System.out.println(emp.getEmpno() + "\t" + emp.getEname() + "\t" + emp.getSal() + "\t" + emp.getDept().getDname() + "\t" + emp.getDept().getLoc()); } }); }
-
映射文件
<!-- EmpMapper.xml中 --> <mapper namespace="com.lwclick.mapper.EmpMapper"> <sql id="empCols"> empno, ename, sal, hireDate, deptno </sql> <select id="findAll" resultType="employee"> SELECT <include refid="empCols" /> FROM emp </select> </mapper> <!-- DeptMapper.xml中 --> <mapper namespace="com.lwclick.mapper.DeptMapper"> <select id="findById" resultType="dept"> SELECT * FROM dept WHERE deptno = #{param1} </select> </mapper>
实现功能2(一对多):
-
测试类:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class); DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class); // 此处应查询所有部门数据,通过循环,获取每个部门的编码 int deptno = 10; Dept dept = deptMapper.findById(deptno); // 通过部门的编码,查询该部门下所有的员工 List<Employee> empList = empMapper.findByDeptno(dept.getDeptno()); // 赋给该部门的员工list dept.setEmployeeList(empList); DBUtil.closeSqlSession(sqlSession); System.out.println(dept.getDeptno() + "\t" + dept.getDname() + "\t" + dept.getLoc()); for (Employee employee : dept.getEmployeeList()) { System.out.println("\t" + employee); } }
-
映射文件:
<!-- EmpMapper.xml中 --> <select id="findByDeptno" resultType="employee"> SELECT <include refid="empCols" /> FROM emp WHERE deptno = #{param1} </select>
1.3 级联查询
级联查询:利用数据库表间的外键关联关系,进行自动的级联查询操作
使用MyBatis实现级联查询,除了要在 实体类中增加关联属性外,还需要在映射文件中进行配置
1.3.1 立即加载
实现功能1(多对一):
-
测试类:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class); // 无需组装!!!!!! List<Employee> employeeList = empMapper.findAll(); DBUtil.closeSqlSession(sqlSession); employeeList.forEach(emp -> { if (emp.getDept() == null) { System.out.println(emp.getEmpno() + "\t" + emp.getEname() + "\t" + emp.getSal() + "\t 无部门"); } else { System.out.println(emp.getEmpno() + "\t" + emp.getEname() + "\t" + emp.getSal() + "\t" + emp.getDept().getDname() + "\t" + emp.getDept().getLoc()); } }); }
-
映射文件(多对一),使用 resultMap,结合
association
:<select id="findAll" resultMap="empResultType"> <!-- 返回类型改为 resultMap --> SELECT <include refid="empCols" /> FROM emp </select> <!-- 配置 resultMap --> <resultMap id="empResultType" type="employee"> <!-- ================ 如果属性名称,除大小写外都一致,无需手动映射 ============= // 映射主键 <id property="empno" column="empno" /> // 映射非主键列 property:实体类的属性名 column:数据库表的列名 <result property="ename" column="ename" /> <result property="job" column="job" /> <result property="hireDate" column="hireDate" /> <result property="sal" column="sal" /> --> <!-- 下面也用到了 deptno,所以此处要指定一下 --> <result property="deptno" column="deptno" /> <!-- 配置【多对一、一对一】的关联映射 --> <!-- column:是当前表emp的deptno --> <association property="dept" column="deptno" select="com.lwclick.mapper.DeptMapper.findById" /> <!-- select:执行哪个文件下的什么SQL --> </resultMap>
实现功能2(一对多):
-
测试类:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class); // 不再手动组装 int deptno = 10; Dept dept = deptMapper.findById(deptno); DBUtil.closeSqlSession(sqlSession); System.out.println(dept.getDeptno() + "\t" + dept.getDname() + "\t" + dept.getLoc()); for (Employee employee : dept.getEmployeeList()) { System.out.println("\t" + employee); } }
-
映射文件(一对多),使用 resultMap,结合
collection
:<select id="findById" resultMap="deptResultMap"> <!-- 返回类型改为 resultMap --> SELECT * FROM dept WHERE deptno = #{param1} </select> <resultMap id="deptResultMap" type="dept"> <!-- 由于两个表中都有 deptno,所以此处要指定一下 --> <id property="deptno" column="deptno" /> <!-- 配置【一对多】的关联映射 --> <!-- column是当前表 dept的deptno --> <!-- ofType:集合中元素的数据类型 --> <collection property="employeeList" column="deptno" ofType="employee" select="com.lwclick.mapper.EmpMapper.findByDeptno" /> </resultMap>
1.3.2 理解 resultMap
在单表情况下,数据库字段名和实体类属性如果一致(不考虑大小写),还可以自动映射Auto-Mapping
在复杂的情况下,就需要对查询结果进行手动映射
<resultMap id="" type="">
<!-- 【主键】映射 -->
<id property="" javaType="" column="" jdbcType="" typeHandler=""></id>
<!-- 【非主键】映射 -->
<result property="" column=""></result>
<!-- 【多对一、一对一】映射 -->
<association property="" column="" fetchType="" select=""></association>
<!-- 【一对多】映射 -->
<collection property="" column="" fetchType="" select="" ofType=""></collection>
</resultMap>
resultMap 元素的属性:
- id:resultMap 的唯一标识
- type:结果最终形成哪个类的对象,和 resultType一样
resultMap 子元素:
- id:配置主键的数据库列和对象属性的关系
- property:需要映射到 JavaBean 的属性名称
- column:数据表的列名或者列别名
- result:配置非主键列和属性
- property、column
association
:配置多对一、一对一关联字段列和属性,对应一个对象属性- property、column
- fetchType:自动延迟加载
select
:使用哪个查询获取属性的值,要求指定namespace+id的全名称
collection
:配置一对多、多对多关联字段和属性,对应一个集合属性- property、column、fetchType、
select
- ofType:指明集合中元素的类型
- property、column、fetchType、
注意
:在resultMap手动映射中,一个关联列可能对应多个property,都要进行手动映射(比如 deptno字段)。其他列名和属性名一致的,都可以省略
1.3.3 延迟加载
多表查询中,如果第一个查询结果有 N条记录,随后根据这 N条记录的值,再去查询数据库 N次,共计N+1次查询,对服务器压力很大
- 解决方法1:延迟加载。关联表的数据,只有等到真正使用的时候才查询,不用不查询
- 解决方法2:连接查询。一条SQL语句,查询到所有的数据
延迟加载:设置为延迟加载的内容,等到真正使用时才进行查询(比如查询所有员工信息时,只需要显示员工信息,而不显示部门信息,那么部门信息就可以设置默认不查询,等真正要用的时候,再进行查询,提高了数据库的新能)。多用在关联对象和集合中
延迟加载的设置:
-
全局开关:在 mybatis-cfg.xml中打开延迟加载的开关(打开后,所有的 association和 collection都生效)
<settings> <!-- 配置延迟加载的开关 --> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> </settings>
- lazyLoadingEnabled(默认true):是否开启延迟加载。开启后所有关联对象都会延迟加载, fetchType属性会覆盖该项的开关
- aggressiveLazyLoading(默认true):开启时,任何方法的调用都会加载懒加载对象的所有属性。否则,每个属性按需加载
-
分开关:指定的某个 association和 collection元素中,配置
fetchType
,取值 eager | lazy ,会覆盖全局延迟设置<resultMap id="deptResultMap" type="dept"> <id property="deptno" column="deptno"/> <!-- fetchType,配置是否开启延迟加载 --> <collection property="employeeList" column="deptno" fetchType="lazy" select="com.lwclick.mapper.EmpMapper.findByDeptno" /> </resultMap>
-
eager(
association 默认
):立即加载 -
lazy(
collection 默认
):延迟加载
功能1(fetchType=“eager”):
功能1(fetchType=“lazy”):
-
测试类:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class); List<Employee> employeeList = empMapper.findAll(); DBUtil.closeSqlSession(sqlSession); employeeList.forEach(emp -> { System.out.println(emp.getEmpno() + "\t" + emp.getEname() + "\t" + emp.getSal()); // 不输出部门信息(模拟不使用部门的数据) }); }
-
映射文件:
<resultMap id="empResultType" type="employee"> <result property="deptno" column="deptno" /> <!-- 使用 fetchType="lazy" 设置懒加载 --> <association property="dept" column="deptno" select="com.lwclick.mapper.DeptMapper.findById" fetchType="lazy"/> </resultMap> <select id="findAll" resultMap="empResultType"> SELECT <include refid="empCols" /> FROM emp </select>
1.4 连接查询
多表连接查询(join)只需执行一条 SQL语句,就可以查询到多张表的数据,效率很高,但是会有大量重复的数据
以上图可知,每一条记录有多个表的字段,一般情况下会转换并组装到一个对象中。
- 那么要么是组装到 7 个 Employee对象中,每个 Employee对象中还包含其所属的 Dept信息
- 要么是组装到 3 个Dept对象中,每个 Dept对象有该部门所有的 Employee员工信息
多表连接查询:除了必须提供多表连接查询的SQL语句外,关键还需要使用 resultMap进行结果映射
实现功能1(多对一):
-
测试类:
@Test public void testFindAll() { SqlSession sqlSession = DBUtil.getSqlSession(); EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class); List<Employee> employeeList = empMapper.findAll(); DBUtil.closeSqlSession(sqlSession); employeeList.forEach(emp -> { // 输出数据 }); }
-
映射文件:
<resultMap id="empResultType" type="employee"> <!-- 多表连接查询时, resultMap的自定义映射中,【各个属性和列的映射不可缺少】,不会自动映射 --> <id property="empno" column="empno" /> <result property="ename" column="ename" /> <result property="sal" column="sal" /> <result property="deptno" column="deptno" /> <!-- 多表连接查询时,【不用写 select属性】 --> <association property="dept" column="deptno"> <id property="deptno" column="deptno" /> <result property="dname" column="dname" /> <!-- SQL查出来的数据,必须进行配置 --> <result property="loc" column="loc" /> </association> </resultMap> <select id="findAll" resultMap="empResultType"> SELECT e.empno, e.ename, e.sal, e.deptno, d.dname, <!-- 需要在 association中进行配置,重点在 column属性--> d.loc FROM `emp` e LEFT JOIN dept d ON e.DEPTNO = d.DEPTNO </select>
实现功能2(一对多):
-
测试类:
@Test public void testFindAll2() { SqlSession sqlSession = DBUtil.getSqlSession(); DeptMapper deptMapper = sqlSession.getMapper(DeptMapper.class); Dept dept = deptMapper.findById(10); DBUtil.closeSqlSession(sqlSession); // 输出数据 }
-
映射文件:
<resultMap id="deptResultMap" type="dept"> <id property="deptno" column="deptno"/> <result property="dname" column="dname" /> <result property="loc" column="loc" /> <collection property="employeeList" ofType="employee"> <!-- 需要使用 ofType指定返回的JavaBean类型!!--> <id property="empno" column="empno" /> <result property="ename" column="ename" /> <result property="job" column="job" /> <result property="sal" column="sal" /> </collection> </resultMap> <select id="findById" resultMap="deptResultMap"> SELECT d.deptno, d.dname, d.loc, e.empno, e.ename, e.job, e.sal FROM dept d LEFT JOIN emp e ON e.DEPTNO = d.DEPTNO WHERE d.deptno = #{param1} </select>
注意
:
- 多表连接查询时只有立即加载;
- ResultMap中各列无法省略、必须配置,省略的不会自动映射
- 使用 collection 配置一对多映射时,要使用
ofType
指定返回的JavaBean类型
1.5 总结与扩展
resultType 和 resultMap使用场景:
- resultType
- 单表查询,且封装的实体和数据库字段一一对应
- resultMap
- 实体封装的属性和数据库字段不一致
- 使用 N+1 级联查询时
- 多表连接查询时
一对一关联映射的实现:
- 实例:学生和学生证、雇员和工牌
- 数据库层次:主键关联或外键关联
- MyBatis层次:两边的映射文件(学生、学生证)中,都使用 association 即可(学生中使用association关联学生证,学生证同理)
多对多关系映射的实现:
- 实例:学生和课程
- 数据库层次:引入一个中间表或将一个多对多转为两个一对多
- MyBatis层次:两边的映射文件(学生、课程)中,都使用 collection 即可
自关联映射的实现:
-
实例:Emp 表中的员工和上级(一般是一对多关联)
-
数据库层次:当前表外键参考当前表主键(mgr 参考 empno)
-
MyBatis层次:按照一对多处理,但是增加的属性都写到一个实体类中,增加的映射也都写到一个映射文件中(例如获取上级时,只有一个,那就用 association,获取下级时,有多个,使用 collection)
public class Employee { private Employee emp; private List<Employee> empList; // 其他省略 }
2. 注解开发
2.1 简单介绍
除了使用 xml文件完成功能的开发,还可以使用注解的方式。
当可以使用 AutoMapping(返回格式是 resultType)时,使用注解非常简单;当必须配置 resultMap时,还是推荐使用 xml配置文件的方式
不管使用纯注解、纯XML,还是两者结合的方式(如果某个功能同时用两种方式配置,XML生效),在MyBatis的配置文件中,都推荐使用 <package name="" />
的方式加载映射文件
示例:使用注解完成对 emp表的 CRUD操作(测试类代码省略,无映射文件)
public interface EmpMapper {
@Select("select * from emp")
public List<Employee> findAll();
@Select("select * from emp where empno = #{param1}")
public Employee findById(int empno);
@Select(value= "select * from emp where job =#{param1} and deptno = #{param2}")
public List<Employee> findEmp(String job, int deptno);
@Insert("insert into emp values(null,#{ename},#{job}," +
"#{mgr},#{hireDate},#{sal},#{comm},#{deptno})")
int saveEmp(Employee emp);
@Update("update emp set job =#{job},sal =#{sal} where empno =#{empno}")
int updateEmp(Employee emp);
@Delete("delete from emp where empno = #{param1}")
int deleteEmp(int empno);
}
2.2 简单扩展
如果想用注解实现 resultMap的功能,可以使用 @Results 注解实现
-
如果对象中关联了集合类型对象,通过 @Results的 many属性配置(只能实现 N+1查询方式)
-
如果对象中关联了另一个对象,通过 @Results的 one属性配置
@Results(id = "findAllRes", value = { @Result(property = "deptno", column = "empno", id = true), @Result(property = "dname", column = "ename"), @Result(property = "empList", column = "deptno", many = @Many(select = "findAll")) }) Dept findById(int deptno);
在 MyBatis3中,新增了@SelectProvider、@UpdateProvider、@DeleteProvider、@InsertProvider,统称@SqlProvider。当使用这些注解时将不在注解中直接编写SQL,而是调用某个类的特定方法形成的SQL。
// ==================== 接口中 ======================
@SelectProvider(type= MyProvider.class, method = "selectStudents")
List<Student> selectStudent();
// ==================== 自定义的类 ======================
public class MyProvider {
public String selectStudents(){
return "select * from student";
}
}
2.3 注解与XML
在 MyBatis 中更推荐使用 XML配置的方式
XML | 注解 | |
---|---|---|
优点 | 1.类和类之间的解耦 2.利于修改。直接修改XML文件,无需到源代码中修改 3.配置集中在XML中,对象间关系一目了然,利于快速了解项目和维护 4.容易和其他系统进行数据交换 | 1.简化配置 2.使用起来直观且容易,提升开发效率 3.类型安全,编译器进行校验,不用等到运行期才会发现错误 4.注解的解析可以不依赖于第三方库,可以直接使用Java自带的反射 |
3. MyBatis 运行原理
3.1 构建 SqlSessionFactory
InputStream is = Resources.getResourceAsStream("mybatis-cfg.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
- 通过XMLConfigBuilder解析全局配置文件,读出参数,并将读取的内容存入Configuration对象中。Configuration采用单例模式,只有一份
- 使用Configuration去创建SqlSessionFactory对象。SqlSessionFactory是一个接口,这里返回的是实现类DefaultSqlSessionFactory。此处使用的构建者(Builder)模式。对于复杂对象的创建,使用构造方法很难实现,可以使用构建者模式
通过跟踪发现,最终映射文件的信息存储到了Configuration的mappedStatements成员变量中
3.2 创建 SqlSession
SqlSession sqlSession = factory.openSession();
- SqlSession是一个接口,这里返回的是其实现类DefaultSqlSession的一个实例。
- 其中有一个Executor类型的成员变量。其实进行数据库操作时SqlSession就是一个门面,真正
干活的是Executor
。- Executor:执行器,由它来调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL
- StatementHandler:使用数据库的Statemet(PreparedStatement)执行操作(最重要的)
- ParameterHandler:处理SQL参数
- ResultSetHandler:处理结果集ResultSet的封装返回
3.3 创建 Mapper代理对象并执行CRUD
创建 Mapper对象:
EmpMapper empMapper = sqlSession.getMapper(EmpMapper.class);
底层使用了动态代理模式。
执行数据库的CRUD操作:
List<Employee> employeeList = empMapper.findAll();
跟踪源码,发现其实还是调用了SqlSession的selectList()、selectOne()、update()等方法
3.4 使用 SqlSession 执行 CRUD操作
session.selectList("com.bjsxt.mapper.EmployeeMapper.selectAll");
session.selectOne("com.bjsxt.mapper.EmployeeMapper.selectById", 7839);
session.update("com.bjsxt.mapper.EmployeeMapper.updateEmp", emp);
经过跟踪代码发现,SqlSession的selectOne()、selectMap()方法的底层都调用了selectList()
方法,而insert()、delete()底层都调用了update()
方法
执行过程中统领全局的代码都在Executor中,比如下图的doUpdate()
(最底层的DML方法)是完成insert、update、delete操作的,而doQuery()
(最底层的查询方法)是完成select操作的
-
获取数据库连接,getConnection()
-
调用
StatementHandler
的**prepare()**方法,创建Statement对象并设置其超时、获取的最大行数等 -
调用StatementHandler的**parameterize()**方法。负责将具体的参数传递给SQL语句
-
其中用到了ParameterHandler的setParameters()和TypeHandler的setParameter()
-
-
调用StatementHandler的**query()或update()**方法。完成查询/DML操作,并对结果集进行封装处理,然后返回最终结果
-
具体实现细节在ResultSetHandler的handleResults()方法中
-
总体步骤图:
3.5 缓存的使用
-
查询二级缓存
-
如果二级缓存中没有,就查询一级缓存;如果一级缓存没有就查询数据库
-
将从数据库查询的数据放入一级缓存
-
DML操作会刷新缓存
4. MyBatis 相关面试题
-
为什么说Mybatis是半自动ORM映射工具?它与全自动的区别在哪里?
- 还需要编写SQL语句
-
#{}和${}的区别是什么?
- 占位符还是字符串拼接
-
Mybatis是否支持延迟加载?如果支持,它的实现原理是什么?
-
CGLIB
-
最佳实践中,通常一个Xml映射文件,都会写一个Dao接口与之对应,请问,这个Dao接口的工作原理是什么?Dao接口里的方法能重载吗?
- 动态代理
-
Mybatis动态SQL是做什么的?有哪些动态 SQL?动态SQL的执行原理
-
OGNL
-
MyBatis的基本工作流程
-
简述Mybatis的Xml映射文件和Mybatis内部数据结构之间的映射关系?
Mybatis将所有Xml配置信息都封装到All-In-One重量级对象Configuration内部。 在Xml映射文件中,<parameterMap>标签会被解析为ParameterMap对象,其每个子元素会被解析为ParameterMapping对象。 <resultMap>标签会被解析为ResultMap对象,其每个子元素会被解析为ResultMapping对象。 每一个<select>、<insert>、<update>、<delete>标签均会被解析为MappedStatement对象,标签内的sql会被解析为BoundSql对象。
-
一对一、一对多、多对多的关联查询映射
-
一级和二级缓存
- 使用缓存读取的过程:先二级再一级
-
Mybatis的插件运行原理,以及如何编写一个插件
- Mybatis仅可以编写针对 ParameterHandler、ResultSetHandler、StatementHandler、Executor 这 4 种接口的插件,Mybatis 通过动态代理为需要拦截的接口生成代理对象以实现接口方法拦截功能