MyBatis篇
Mybatis介绍
回顾:JDBC有哪些不足?
- 数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
- Mybatis解决:在
mybatis-config.xml
中配置数据库连接池,使用连接池管理数据库连接。
- Mybatis解决:在
- Sql 语句写在代码中造成代码不易维护,实际应用 sql 变化的可能较大,sql 变动需要改变 java 代码。
- Mybatis解决:将 Sql 语句配置在 xxxxmapper.xml 文件中与 java 代码分离。
- 向 Sql 语句传参数麻烦,因为 Sql 语句的 where 条件不一定,可能多也可能少,占位符需要和参数一一对应。
- Mybatis解决:Mybatis 自动将 Java 对象映射至 Sql 语句。
- 对结果集解析麻烦, Sql 变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成 Pojo 对象解析比较方便。
- Mybatis解决:Mybatis 自动将 Sql 执行结果映射至 Java 对象。
什么是Mybatis?
(1)Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接、创建statement等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能,灵活度高。
(2)MyBatis 可以使用 XML 或注解来配置和映射原生信息,将 POJO映射成数据库中的记录,避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。
(3)通过xml 文件或注解的方式将要执行的各种 statement 配置起来,并通过java对象和statement中sql的动态参数进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射为java对象并返回。(从执行sql到返回result的过程)。
Mybatis工作流程
使用MyBatis概括来看大致包括如下几步:
-
用户定义DAO接口,配置相关的xml文件信息【疑问:Dao接口和XML文件的SQL如何建立关联?】
-
加载配置文件。需要加载的配置文件包括全局配置文件(SqlMapConfig.xml)和SQL(Mapper.xml)映射文件。
-
解析SQL(Mapper.xml)映射文件 --> SqlSource。
- 创建SqlSource:xml文件中的每个SQL标签都会封装成 SqlSource 对象,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL。重点:这个过程创建了SqlSource对象,并且最后作为MappedStatement的属性存储在MappedStatement对象中。
- 根据SQL语句的不同,又分为动态SQL和静态SQL。其中,静态SQL包含一段String类型的sql语句;而动态SQL则是由一个个SqlNode组成。
- 底层代码实现:在mybatis启动时,会加载xml文件并进行解析,XMLMapperBuilder#configurationElement会去解析一个xml文件中所有的节点:namespace、cache-ref、cache等。
-
创建会话工厂。MyBatis通过读取配置文件的信息来构造出会话工厂(SqlSessionFactory)。
-
创建会话。拥有了会话工厂,MyBatis就可以通过它来创建会话对象(SqlSession)。会话对象是一个接口,该接口中包含了对数据库操作的增删改查方法。
-
创建执行器。因为会话对象本身不能直接操作数据库,所以它使用了一个叫做数据库执行器(Executor)的接口来帮它执行操作。
-
操作数据库,MappedStatement 内部保存的所要执行的 SqlSource 方法(保存有从Mapper.xml中解析出来的Sql片段信息),最终通过 Executor 的 query() 方法来根据Sql片段信息执行数据库操作。
-
结果映射:将操作数据库的结果按照映射的配置进行转换,可以转换成HashMap、JavaBean或者基本数据类型,并将最终结果返回。
底层逻辑
SqlSession 委托 Executor 执行数据库
创建SqlSource和MappedStatement
Executor 的执行操作
Mapper接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为key值,可唯一定位一个MapperStatement。在Mybatis中,每一个<select>
、<insert>
、<update>
、<delete>
标签,都会被解析为一个MapperStatement对象。
Mapper 接口的工作原理是JDK动态代理,Mybatis运行时会使用JDK动态代理为Mapper接口生成代理对象proxy,代理对象会拦截接口方法,转而执行MapperStatement所代表的sql,然后将sql执行结果返回。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到namespace为com.mybatis3.mappers.StudentDao下面 id 为 findStudentById 的 MapperStatement。
简单总结MyBatis初始化过程
在 MyBatis 初始化过程中,会加载mybatis-config.xml配置文件、Mapper.xml映射配置文件以及Mapper接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration 对象中。
- 解析 mybatis-config.xml 配置文件
- SqlSessionFactoryBuilder
- XMLConfigBuilder
- Configuration
- 解析 Mapper.xml 映射配置文件
- XMLMapperBuilder::parse()
- XMLStatementBuilder::parseStatementNode()
- XMLLanguageDriver
- SqlSource
- MappedStatement
- 解析Mapper接口中的注解
- MapperRegistry
- MapperAnnotationBuilder::parse()
MyBatis的功能架构
Mybatis的功能架构分为三层:
- API接口层:提供给外部使用的接口API,开发人员通过这些本地API来操纵数据 库。接口层一接收到调用请求就会调用数据处理层来完成具体的数据处理。
- 数据处理层:负责具体的SQL查找、SQL解析、SQL执行和执行结果映射处理等。它主要的目的是根据调用的请求完成一次数据库操作。
- 基础支撑层:负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
Mybatis缓存
Mybatis的一级缓存原理(sqlsession级别)
在应用运行过程中,我们有可能在一次数据库会话中,执行多次查询条件完全相同的 SQL,MyBatis 提供了一级缓存的方案优化这部分场景,如果是相同的 SQL 语句,会优先命中一级缓存,避免直接对数据库进行查询,提高性能。
每个 SqlSession 中持有了 Executor,每个 Executor 中有一个 LocalCache。当用户发起查询时,MyBatis 根据当前执行的语句生成 MappedStatement,在 Local Cache 进行查询,如果缓存命中的话,直接返回结果给用户,如果缓存没有命中的话,查询数据库,结果写入 Local Cache,最后返回结果给用户。具体实现类的类关系图如下图所示:
二级缓存原理(mapper级别)
如果多个 SqlSession之间需要共享缓存,则需要使用到二级缓存。开启二级缓存后,会使用 CachingExecutor 装饰Executor,进入一级缓存的查询流程前,先在 CachingExecutor 进行二级缓存的查询,具体的工作流程如下所示。
二级缓存开启后,同一个 namespace 下的所有操作语句,都影响着同一个 Cache,即二级缓存被多个 SqlSession 共享,是一个全局的变量。
<?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.xunqi.gulimall.seckill.mapper.GoodsMapper">
当开启缓存后,数据的查询执行的流程为:二级缓存 -> 一级缓存 -> 数据库
- MyBatis 的二级缓存相对于一级缓存来说,实现了 SqlSession 之间缓存数据的共享,同时粒度更加细,能够到 namespace 级别,通过 Cache 接口实现类不同的组合,对 Cache 的可控性也更强。
- MyBatis 在多表查询时,极大可能会出现脏数据,有设计上的缺陷,安全使用二级缓存的条件 比较苛刻。
- 在分布式环境下,由于默认的 MyBatis Cache 实现都是基于本地的,分布式环境下必然会出现读取到脏数据,需要使用集中式缓存将 MyBatis 的 Cache 接口实现,有一定的开发成本,直接使用 Redis、Memcached 等分布式缓存可能成本更低,安全性也更高。
既然有了一级缓存,那么为什么要提供二级缓存昵?
- 二级缓存是 mapper 级别的缓存,多个SqISession去操作同一个Mapper的sql语句,多个SqISession可以共
用二级缓存,二级缓存是跨SqISession的。二级缓存的作用范围更大。- 还有一个原因,实际开发中, MyBatis 通常和 Spring进行整合开发。Spring将事务放到Service 中管理,对于每一个service 中的SqISession是不同的,这是通过mybatis-spring中的org.mybatis.spring.mapper.MapperScannerConfigurer 创建SqISession 自动注入到service 中的。每次查询之后都要进行关闭SqISession, 关闭之后数据被清空。所以spring整合之后,如果没有事务,一级缓存是没有意义的。
Mybatis底层:#{} PreparedStatement预编译
PreparedStatement的预编译功能是指首先将SQL语句编译成可重复使用的预处理语句(PreparedStatement),然后将其保存在数据库服务器端的缓存中以备后续使用。当需要执行该SQL语句时,只需将具体参数传入预处理语句即可快速执行SQL语句,而无需每次都重新解析和编译SQL语句。
检查 >>> 编译 >>> 执行
JDBC 中使用对象 PreparedStatement 来抽象预编译语句,使用预编译。预编译阶段可以优化 SQL 的执行。预编译之后的 SQL 多数情况下可以直接执行,DBMS 不需要再次编译,越复杂的SQL,编译的复杂度将越大,预编译阶段可以合并多次操作为一个操作。同时预编译语句对象可以重复利用。把一个 SQL 预编译后产生的 PreparedStatement 对象缓存下来,下次对于同一个SQL,可以直接使用这个缓存的 PreparedState 对象。Mybatis默认情况下,将对所有的 SQL 进行预编译。
Mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值,有效的防止SQL注入,提高系统安全性。
Mybatis分页
Mybatis使用RowBounds对象进行分页,它是针对ResultSet结果集执行的内存分页,而非物理分页。可以在sql内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
四种分页:
- SQL分页:limit #{currIndex} , #{pageSize}
- 拦截器分页
- 前端分页
- RowBounds分页
PageHelper的分页原理主要基于MyBatis的插件机制。具体来说,PageHelper内部实现了一个PageInterceptor拦截器,这个拦截器会在MyBatis执行SQL查询之前进行拦截。
- 当我们在代码中调用PageHelper的startPage方法时,它会在当前线程上下文中设置一个ThreadLocal变量,用于保存分页的参数,如当前页码、每页显示的数量等。
- 随后,当MyBatis执行SQL查询时,PageInterceptor拦截器会拦截到这一操作。拦截器会从ThreadLocal中获取到分页参数,并根据这些参数来改写原始的SQL语句,添加LIMIT和OFFSET子句,以实现分页查询。
- 改写后的SQL语句会被发送到数据库执行,数据库返回的结果集就是根据分页参数查询得到的结果。
- 最后,PageInterceptor拦截器会将ThreadLocal中的分页参数清除,避免对后续操作产生影响。
通过这种方式,PageHelper实现了对MyBatis查询结果的分页处理,而无需修改原有的SQL语句、Mapper接口和XML文件,因此具有无侵入性和易用性。同时,由于分页操作是在数据库层面进行的,因此也具有较高的性能。
需要注意的是,PageHelper使用了ThreadLocal来保存分页参数,因此分页参数是与线程绑定的,这意味着不同的线程之间不会共享分页参数,从而保证了分页的准确性和独立性。
Mybatis延迟加载
Mybatis仅支持association关联对象和collection关联集合对象的延迟加载,association指的就是一对一,collection指的就是一对多查询。在Mybatis配置文件中,可以配置是否启用延迟加载lazyLoadingEnabled=true|false。例如在进行一对多查询的时候,只查询出一方,当程序中需要多方的数据时,Mybatis 再发出sql语句进行查询,这样子延迟加载就可以的减少数据库压力。Mybatis 的延迟加载只是对关联对象的查询有迟延设置,对于主加载对象都是直接执行查询语句的。
MyBatis根据对关联对象查询的select语句的执行时机,分为三种类型:直接加载、侵入式延迟加载与深度延迟加载。
- 直接加载:执行完对主加载对象的 select 语句,马上执行对关联对象的 select 查询。
- 侵入式延迟: 执行对主加载对象的查询时,不会执行对关联对象的查询。但当要访问主加载对象的详情属性时,就会马上执行关联对象的select查询。
- 深度延迟: 执行对主加载对象的查询时,不会执行对关联对象的查询。访问主加载对象的详情时也不会执行关联对象的select查询。只有当真正访问关联对象的详情时,才会执行对关联对象的 select 查询。
需要注意的是, 延迟加载的应用要求,关联对象的查询与主加载对象的查询必须是分别进行的 select 语句,不能是使用多表连接所进行的select查询。因为多表连接查询,其实质是对一张表的查询,对由多个表连接后形成的一张表的查询。会一次性将多张表的所有信息查询出来。
Mybatis的优缺点
四大组件Executor 、StatementHandler 、ParameterHandler 、ResultSetHandler:
- ParameterHandler :处理SQL的参数对象
- ResultSetHandler:处理SQL的返回结果集
- StatementHandler :数据库的处理对象,用于执行SQL语句
- Executor:Mybatis的执行器,用于执行增删改查操作
优点:
(1)基于SQL语句编程,相当灵活,不会对应用程序或者数据库的现有设计造成任何影响,SQL写在XML里,解除sql与程序代码的耦合,便于统一管理;提供XML标签,支持编写动态SQL语句,并可重用。
(2)与JDBC相比,减少了50%以上的代码量,消除了JDBC大量冗余的代码,不需要手动开关连接;
(3)很好的与各种数据库兼容(因为MyBatis使用JDBC来连接数据库,所以只要JDBC支持的数据库MyBatis都支持)。
(4)能够与Spring很好的集成;
(5)提供映射标签,支持对象与数据库的ORM字段关系映射;提供对象关系映射标签,支持对象关系组件维护。
缺点
(1)SQL语句的编写工作量较大,尤其当字段多、关联表多时,对开发人员编写SQL语句的功底有一定要求。
(2)SQL语句依赖于数据库,导致数据库移植性差,不能随意更换数据库。