如果不使用Mybatis,我们可以直接使用JDBC来操作数据库,可以使用Hibernate,也可以使用JPA或者Spring 的JdbcTemplate等ORM框架来操作数据库,这几个相对比JDBC过于繁琐,重复代码太多,难以维护,现在我们很少直接用。而Hibernate框架封装程度太高,不利于优化,也不支持动态SQL,灵活性差。
Spring JDBCTemplate比较小巧,对于数据操作相对简单固定的场景,该小巧的框架就够了。
而JPA是Hibernate的一个子集,很多兼有Hibernate的封装和Mybatis的灵活,也是用的比较多的orm框架。
Mybatis是一个半自动框架,封装程度没有Hibernate那么高,不会自动生成全部的SQL语句,但是大部分重复工作都能自动生成,因此灵活性也很高。
Mysql的核心特征主要包括以下问题:
1.使用连接池对连接进行管理,
2.SQL和代码分离,结果集映射,.参数映射
3.动态SQL
4.缓存管理
5.插件机制
下面先分析一下前三个,后两个比较复杂,后面章节单独讨论。
ORM和连接池是独立的,不用orm也可以直接使用连接池。
连接池可以减少连接创建和释放的开销,提高处理性能。
问题:有连接池的时候如何保证web请求的无状态的?
SpringMVC响应请求时无状态的,但是到了dao层,数据库连接池是有状态的,两者怎么管理的呢
每个请求会独占一个连接池的连接,用完就归还,多个请求之间不会相互影响,这样就保证了每个请求都是无状态的。
Mybatis自带了两种Unpooled和pooled,但是一般我们会用更流程的开源连接池软件例如hikaCP或者Druid等。
这个主要是将SQL写在XML中,也就是经常见到的各种**Map.xml,代码通过路径查找和映射。SQL语句和配置信息都写在XML文件中,代码只负责对象的管理和业务的实现,从而实现两者的分离,但是执行时仍能交互。
为了保证准确执行,Mybatis还提供了参数集和结果集的映射方法,就是当前的入参和出参与代码中的哪个对象关联的。所以当我们自己写xml的时候,一般先将需要的Dto对象,实体对象等都先定义好。
由于业务传入的查询参数可能不同,这导致最终执行的SQL会有差异,为了让程序更具适用性,可以在XML映射文件里添加 if else,choose,trim 和foreach等完成条件过滤。
MyBatis 的整体架构分为三层 , 分别是基础支持层 、 核心处理层和接口层,如下图所示:
下面分别看每个层次的功能。
基础支持层包含整个 MyBatis 的基础模块,这些模块为核心处理层的功能提供了良好的支撑。下面简单梳理各个模块的功能。
1.反射模块
Java 中的反射虽然功能强大,但对大多数开发人员来说,写出高质量的反射代码还是有一定难度的 。 MyBatis 中专门提供了反射模块,该模块对 Java 原生的反射进行了良好的封装,提供了更加简洁易用的 API,方便上层使调用,并且对反射操作进行了一系列优化,例如缓存了类的元数据,提高了反射操作的性能 。
2.类型转换模块
MyBatis 为简化配置文件提供了别名机制 , 该机制是类型转换模块的主要功能之一 。 类型转换模块的另一个功能是实现 JDBC 类型与 Java 类型之间的转换,该功能在为 SQL 语句绑定实参以及映射查询结果集时都会涉及。在为 SQL 语句绑定实参时, 会将数据由 Java 类型转换成 JDBC 类型;而在映射结果集时,会将数据由 JDBC 类型转换成 Java 类型。
3.日志模块
无论在开发测试环境中,还是在线上生产环境中,日志在整个系统中的地位都是非常重要的。良好的日志功能可以帮助开发人员和测试人员快速定位 Bug 代码,也可以帮助运维人员快速定位性能瓶颈、等问题 。 目前的 Java 世界中存在很多优秀的日志框架,例如 Log4j 、 Log4j2, slf4j 等 。 MyBatis 作为一个设计优良的框架,除了提供详细的日志输出信息,还要能够集成多种日志框架,其日志模块的一个主要功能就是集成第三方日志框架。
4.资源加载模块
资源加载模块主要是对类加载器进行封装,确定类加载器的使用顺序,并提供了加载类文件以及其他资源文件的功能 。
5.解析器模块
解析器模块的主要提供了两个功能 : 一个功能是对 XPath 进行封装,为 MyBatis 初始化时解析 mybatis-config.xml 配置文件以及映射配置文件提供支持;另一个功能是为处理动态 SQL 语句中的占位符提供支持。
6.数据源模块
数据源是实际开发中常用的组件之一。 现在开源的数据源都提供了比较丰富的功能,例如,连接池功能、检测连接状态等,选择性能优秀的数据源组件对于提升 ORM 框架乃至整个应用的性能都是非常重要的。 MyBatis 自身提供了相应的数据源实现,当然 MyBatis 也提供了与第三方数据源集成的接口,这些功能都位于数据源模块之中 。
7.事务管理
MyBatis 对数据库中的事务进行了抽象,其自身提供了相应的事务接口和简单实现。在很多场景中, MyBatis 会与 Spring 框架集成,并由 Spring 框架管理事务.
8.缓存模块
在优化系统性能时 ,优化数据库性能是非常重要的一个环节,而添加缓存则是优化数据库时最有效的手段之一。正确合理地使用缓存可以将一部分数据库请求拦截在缓存这一层,这就能够减少相当一部分数据库的压力。
MyBatis 中提供了一级缓存和二级缓存,而这两级缓存都是依赖于基础支持层中的缓存模块实现的。这里需要读者注意的是, MyBatis 中自带的这两级缓存与 MyBatis 以及整个应用是运行在同一个 川币4 中的,共享同一块堆内存。如果这两级缓存中的数据量较大, 则可能影响系统中其他功能的运行,所以当需要缓存大量数据时 ,优先考虑使用 Redis 、 Memcache 等缓存产品。
9.Binding 模块
通过前面的示例我们知道,在调用 SqISession 相应方法执行数据库操作时,需要指定映射文件中定义的 SQL 节点,如果出现拼写错误,我们只能在运行时才能发现相应的异常 。 为了尽早发现这种错误, MyBatis 通过 Binding 模块将用户自定义的 Mapper 接口与映射配置文件关联起来,系统可以通过调用自定义 Mapper 接口中的方法执行相应的 SQL 语句完成数据库操作,从而避免上述问题。
值得读者注意的是,开发人员无须编写自定义 Mapper 接口的实现, MyBatis 会自动为其创建动态代理对象 。在有些场景中,自定义 Mapper 接口可以完全代替映射配置文件,但有的映射规则和 SQL 语句的定义还是写在映射配置文件中比较方便,例如动态 SQL语句的定义 。
我们来分析 MyBatis 的核心处理层。在核心处理层中实现了 MyBatis 的核心处理流程,其中包括 MyBatis 的初始化以及完成一次数据库操作的涉及的全部流程 。 下面简单描述各个模块的功能,后面会详细分析核心处理层的实现原理 。
1.配置解析
在 MyBatis 初始化过程中,会加载 mybatis-config.xml 配置文件、映射配置文件以及Mapper 接口中的注解信息,解析后的配置信息会形成相应的对象并保存到Configuration 对象中 。 例如定义的<resultMap>节点(即 ResultSet 的映射规则)会被解析成 ResultMap 对象:<result>节点(即属性映射)会被解析成ResultMapping 对象 。 之后,利用该 Configuration 对象创建 Sq!SessionFactory 对象 。待 MyBatis 初始化之后,开发人员可以通过初始化得到 Sq!SessionFactory 创建Sq!Session 对象并完成数据库操作。
2.SOL 解析与 scripting 模块
拼凑 SQL 语句是一件烦琐且易出错的过程,为了将开发人员从这项枯燥无趣的工作中解脱出来, MyBatis 实现动态 SQL 语句的功能,提供了多种动态 SQL 语句对应的节点,例如,<where>节点、<if>节点、<foreach>节点等。通过这些节点的组合使用, 开发人员可以写出几乎满足所有需求的动态 SQL 语句。
MyBatis 中的 scripting 模块会根据用户传入的实参,解析映射文件中定义的动态 SQL节点,并形成数据库可执行的 SQL 语句 。之后会处理 SQL 语句中的占位符,绑定用户传入的实参。
3.SOL 执行
SQL 语句的执行涉及多个组件 ,其中比较重要的是 Executor 、 StatementHandler 、ParameterHandler 和 R巳sultSetHandler 。 Executor 主要负责维护一级缓存和二级缓存,并提供事务管理的相关操作 ,它会将数据库相关操作委托给 StatementHandler 完成。StatementHandler 首先通过 ParameterHandler 完成 SQL 语句的实参绑定,然后通过java.sql.Statement 对象执行 SQL 语句并得到结果集,最后通过 ResultSetHandler 完成结果集的映射,得到结果对象并返回 。下图展示了 MyBatis 执行一条 SQL 语句的大致过程。
4.插件
Mybatis 自身的功能虽然强大,但是并不能完美切合所有的应用场景,因此 MyBatis提供了插件接口,我们可以通过添加用户自定义插件的方式对 MyBati s 进行扩展。用户自定义插件也可以改变 Mybatis 的默认行为,例如,我们可以拦截 SQL 语句并对其进行重写。由于用户自定义插件会影响 MyBatis 的核心行为,在使用自定义插件之前,开发人员需要了解 MyBatis 内部的原理,这样才能编写出安全、高效的插件。
接口层相对简单,其核心是 SqlSession 接口,该接口中定义了 MyBatis 暴露给应用程序调用的 API,也就是上层应用与 MyBatis 交互的桥梁。接口层在接收到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作 。 SqlSession 接口及其具体实现将在后面单独介绍。
加载配置文件并初始化
加载配置文件,配置文件由主配置congfig.xml 以及用户定义的mapper.xml 文件。
加载注解,即主配置中内容解析封装到configuration,以及用户使用的mapper注解或mapperscan,将其mapper信息封装成mappedstatement存储到内存中
接收调用请求
接受用户调用mybatis提供的api,将参数和sql传递数据处理层
处理请求
处理流程如下:
根据sql id寻找对应的MappedStatement对象
根据入参解析MappedStatement对象,得到要执行的sql和传入参数
获取数据库连接,执行sql,并处理结果
根据MappedStatement对象中配置的输出结果进行转换
释放数据库连接资源
4. 返回处理结果
将封装好的结果返回出去
问题:通常一个 Xml 映射文件,都会写一个 Dao 接口与之对应,请问,这个 Dao 接口的工作原理是什么?Dao 接口里的方法,参数不同时,方法能重载吗?
Dao 接口即 Mapper 接口。接口的全限名,就是映射文件中的 namespace 的值;接口的方法名,就是映射文件中 Mapper 的 Statement 的 id 值;接口方法内的参数,就是传递给 sql 的参数。
Mapper 接口是没有实现类的,当调用接口方法时,接口全限名+方法名拼接字符串作为 key 值,可唯一定位一个 MapperStatement。在 Mybatis 中,每一个、、、标签,都会被解析为一个MapperStatement 对象。
举例:com.mybatis3.mappers.StudentDao.findStudentById,可以唯一找到 namespace 为 com.mybatis3.mappers.StudentDao 下面 id 为findStudentById 的 MapperStatement。Mapper 接口里的方法,是不能重载的,因为是使用 全限名+方法名 的保存和寻找策略。Mapper 接口的工作原理是 JDK 动态代理,Mybatis 运行时会使用 JDK动态代理为 Mapper 接口生成代理对象 proxy,代理对象会拦截接口方法,转而执行 MapperStatement 所代表的 sql,然后将 sql 执行结果返回。
Mybatis 使用 RowBounds 对象进行分页,它是针对 ResultSet 结果集执行的内存分页,而非物理分页。可以在 sql 内直接书写带有物理分页的参数来完成物理分页功能,也可以使用分页插件来完成物理分页。
分页插件的基本原理是使用 Mybatis 提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的 sql,然后重写 sql,根据 dialect 方言,添加对应的物理分页语句和物理分页参数。
答:Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true|false。
它的原理是,使用 CGLIB 创建目标对象的代理对象,当调用目标方法时,进入拦截器方法,比如调用 a.getB().getName(),拦截器 invoke()方法发现 a.getB()是null 值,那么就会单独发送事先保存好的查询关联 B 对象的 sql,把 B 查询上来,然后调用 a.setB(b),于是 a 的对象 b 属性就有值了,接着完成 a.getB().getName()方法的调用。这就是延迟加载的基本原理。
当然了,不光是 Mybatis,几乎所有的包括 Hibernate,支持延迟加载的原理都是一样的。
1)一级缓存: 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为Session,当 Session flush 或 close 之后,该 Session 中的所有 Cache 就将清空,默认打开一级缓存。
2)二级缓存与一级缓存其机制相同,默认也是采用 PerpetualCache,HashMap存储,不同在于其存储作用域为 Mapper(Namespace),并且可自定义存储源,如 Ehcache。默认不打开二级缓存,要开启二级缓存,使用二级缓存属性类需要实现 Serializable 序列化接口(可用来保存对象的状态),可在它的映射文件中配置 ;
3)对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了 C/U/D 操作后,默认该作用域下所有 select 中的缓存将被 clear。