MyBatis是个 半自动ORM(对象关系映射)的持久层框架,内部封装了JDBC,并可以将SQL语句单独写在XML配置文件 中,或者使用带有注解的Mapper映射类来完成数据库记录到Java实体的映射。
引用:JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的? 1、数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。 解决:在mybatis-config.xml中配置数据链接池,使用连接池管理数据库连接。 2、Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。 解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。 3、向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。 解决: Mybatis自动将java对象映射至sql语句。 4、对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。解决:Mybatis自动将sql执行结果映射至java对象。
Mapper接口开发指的是,代替传统操作上的Dao层。这里只用写Mapper接口而不用写实现类,是因为Mybatis封装动态代理创建实现类的方法(稍后详解),但是这种接口开发也是需要遵守一定的开发规范。
映射配置文件中的名称空间必须和 Dao 层接口的全类名相同。映射配置文件中的增删改查标签的 id 属性必须和 Dao 层接口的方法名相同。映射配置文件中的增删改查标签的 parameterType 属性必须和 Dao 层接口方法的参数相同。映射配置文件中的增删改查标签的 resultType 属性必须和 Dao 层接口方法的返回值相同。 获取动态代理对象 SqlSession 功能类中的 getMapper() 方法。这个SqlSession .getMapper()方法,里面传的是一个接口的字节码,我们知道,接口中定义的方法必须通过某个类实现该接口,然后创建该类的实例,才能通过 实例调用方法。所以SqISession对象的getMapper()方法返回的一定是某个类的实例.下面介绍四个很重要的相关类。
MapperRegistry类有一个knownMappers属性,用于注册Mapper接口对 应的Class对象和MapperProxyFactory对象之间的关系。另外,MapperRegistry提供了 addMapper() 方法,用于向knownMappers属性中注册Mapper接口信息。在addMapper()方法中,为每个Mapper 接口对应的Class对象创建一个MapperProxyFactory对象,然后添加到knownMappers属性中 最重要的是getMapper()方法,能够根据Mapper接口的Class对象获取对应的 MapperProxyFactory对象,然后就可以使用MapperProxyFactory对象创建Mapper动态代理对象了。
该类是Mapper动态代理工厂类,创建Mapper动态代理类。
该类实现了InvocationHandler接口,到这里就很明显了,这是JDK动态代理。
重写了invoke方法,进行代理操作。而具体的操作在MapperMethod
这里就具体将INSERT,UPDATE,DELETE,SELECT情况划分。该类是对Mapper方法相关信息的封装,通过MapperMethod能够 很方便地获取SQL语句的类型、方法的签名信息等。
这个类和前面四个类是一伙的,这个类是用于描述SQL配置信息,MyBatis框架启动时,XML文件或者注解 配置的SQL信息会被转换为MappedStatement对象注册到Configuration组件中。在Mapper接口开发中,担任读取xml信息。关于Mybatis的整套流程,稍后详解。
总结:MyBatis中Mapper的配置分为两部分,分别为Mapper接口和Mapper SQL配置。MyBatis通 过动态代理的方式创建Mapper接口的代理对象,MapperProxy类中定义了 Mapper方法执行时的拦 截逻辑,通过MapperProxyFactory创建代理实例,MyBatis启动时,会将MapperProxyFactory注册 到Configuration对象中。另外,MyBatis通过MappedStatement类描述Mapper SQL配置信息,框 架启动时,会解析Mapper SQL配置,将所有的MappedStatement对象注册到Configuration对象中。 通过Mapper代理对象调用Mapper接口中定义的方法时,会执行MapperProxy类中的拦截逻 辑,将Mapper方法的调用转换为调用SqlSession提供的API方法。在SqlSession的API方法中通 过Mapper的Id找到对应的MappedStatement对象,获取对应的SQL信息,通过StatementHandler 操作JDBC的Statement对象完成与数据库的交互,然后通过ResultSetHandler处理结果集,将结果 返回给调用者。
再一章节中,可知Mybatis编程步骤大致如下
读取配置文件创建SqlSessionFactory通过SqlSessionFactory创建SqlSession通过sqlsession执行数据库操作通过session提交事务以及关闭会话但这只是外操作,而内部组件很有必要了解其属性及作用,可以说这就是mybatis的精华。
Configuration:用于描述MyBatis主配置文件信息,MyBatis框架在启动时会加载主配置 文件,将配置信息转换为Configuration对象。
SqlSessionFactoryBuilder:根据配置或者代码生成SqlSessionFactory,采用是建筑者模式Java设计模式讲解(设计模式思想很重要,前面已经讲过Mapper接口开发的动态代理模式,Mybatis源码涉及很多模式比如:外观模式、模板模式、工厂模式、装饰器模式等)
SqlSessionFactory:SQL会话工厂,它是一个接口,采用工厂模式创建SqlSession的实例。
SqlSession:面向用户的API,是MyBatis与数据库交互的接口。这里设计到外观模式,虽然SqlSession是MyBatis提供的操作数据库的APL但是真正执行SQL的是Executor组件。
Executor:SqlSession只是面向用户操作数据库的API,但是真正执行sql的是Executor组件,它也是一个接口,有几种不同的实现类。 MyBatis 提供了 3 种不同的 Executor,分别为 SimpleExecutor、ResueExecutor> BatchExecutor, 这些Executor都继承至BaseExecutor, BaseExecutor中定义的方法的执行流程及通用的处理逻辑, 具体的方法由子类来实现,是典型的模板方法模式的应用。SimpleExecutor是基础的Executor,能 够完成基本的增删改査操作,ResueExecutor对JDBC中的Statement对象做了缓存,当执行相同的 SQL语句时,直接从缓存中取出Statement对象进行复用,避免了频繁创建和销毁Statement对象, 从而提升系统性能,这是享元思想的应用。BatchExecutor则会对调用同一个Mapper执行的update> insert和delete操作,调用Statement对象的批量操作功能。另外,我们知道MyBatis支持一级缓存 和二级缓存,当MyBatis开启了二级缓存功能时,会使用CachingExecutor对SimpleExecutor> ResueExecutor> BatchExecutor进行装饰,为查询操作增加二级缓存功能,这是装饰器模式的应用。 Executor实例釆用工厂模式创建,Configuration类提供了一个工厂方法newExecutor(),该方 法返回一个Executor对象。 当new CachingExecutor执行器时,条件是this.cachedEnabled属性为true。这个属性是需要手动配置的,也是开启二级缓存的方法。后面会详细介绍Mybatis缓存机制。
MappedStatement :当我们要执行sql时,得从xml中获取sql配置信息,而MappedStatement用于描述SQL配置信息,MyBatis框架启动时,XML文件或者注解 配置的SQL信息会被转换为MappedStatement对象注册到Configuration组件中。
StatementHandler :封装了对 JDBC Statement 的操作 再继续看源码 在MyBatis工作时,使用的StatementHandler 接口对象实际上就是 RoutingStatementHandler 对象,可以这样理解。
StatementHandler statmentHandler = new RountingStatementHandler();BaseStatementHandler: 是 StatementHandler 接口的另一个实现类.本身是一个抽象类.用于简化StatementHandler 接口实现的难度,属于适配器设计模式体现,它主要有三个实现类
SimpleStatementHandler: 管理 Statement 对象并向数据库中推送不需要预编译的SQL语句 PreparedStatementHandler: 管理 Statement 对象并向数据中推送需要预编译的SQL语句, CallableStatementHandler:管理 Statement 对象并调用数据库中的存储过程。
TypeHandler :类型处理器,用于Java类型与JDBC类型之间的转换。
ParameterHandler:用于处理SQL中的参数占位符,为参数占位符设置值。
ResultSetHandler:封装了对ResultSet对象的处理逻辑,将结果集转换为Java实体对象。
缓存是ORM框架的重要特征,其目的就是为了减少与数据库不必要的交互,提升系统性能。但是在分布式环境下,如果使用不当,则可能会带来数据一致性问题。Mybatis提供了一级缓存和二级缓存。 MyBatis的缓存基于 JVM堆内存实现,即所有的缓存数据都存放在Java对象中。MyBatis通过Cache接口定义缓存对 象的行为。 这些装饰类,各有各的特点及其作用,知道有这个事就行,这里把他们显示出来,主要是想体现这个 装饰器模式。 PerpetualCache 类重写了 Object类的equals()方法,当两个缓存对象的Id相同时,即认为缓存对象相同。另外, PerpetualCache类还重写了 Object类的hashCode()方法,仅以缓存对象的Id作为因子生成hashCode,通过一个HashMap实例存放缓存对象。
一级缓存是默认开启的,是因为Mybatis一些关键特性都是基于Mybatis一级缓存实现的(比如映射)。它是SqlSession级别的缓存,前面介绍核心组件的时候,已经提到过SqlSession是面向用户的API,真正执行sql的还是Executor组件,Executor釆用模板方法设计模式,BaseExecutor类用于处理一些通用的逻辑,其中一级缓存相关的逻辑就是在 BaseExecutor类中完成的。 localCache:Mybatis —级缓存对象 localoutputParametercache:存储过程输出参数缓存
一级缓存大致过程:首先根据缓存key去localCache(Mybatis —级缓存对象)中查,找不到则从数据库获取数据,再将数据写入localCache中,如果查到了,就直接从缓存中获取。这里的缓存key是和Mapper 命名空间以及具体的sql语句以及需要传递的参数等因素去考虑是否相等的。如果有这样的key就是查询到了。 需要注意的是,如果localCacheScope属性设置为STATEMENT,则每次查询操作完成后,都 会调用clearLocalCache()方法清空缓存。除此之外,MyBatis会在执行完任意更新语句后清空缓存 sqlSession.clearCache():只是清除session的一级缓存,二级缓存不清除
<setting name="localCacheScope" value="SESSION"/>localCacheScope:本地缓存作用域(一级缓存SESSION),当前会话的所有数据会保存在会话缓存中 value=" STATEMENT":这不是关闭以及缓存,而是每次查询结束后都会清掉一级缓存。 前面说到一级缓存它是sqlsession级别的缓存,而在一个系统运行中,不可能只有一个sqlsession,如果A sqlsession存的key 对应的结果是张三,但是另一个B sqlSession把查询结果改变成了李四,那么B sqlseesion 建立缓存后按缓存查就会查出李四,但是A sqlsession 它的key并没有变化,因为key对应有缓存,所以他不会去数据库查,而是查他到的缓存还是张三。 因为缓存不能跨会话共享,不同的会话之间对于相同的数据可能有不一样的缓存。在有多个会话或者分布式环境下,会存在脏数据的问题 所以就要用到 mybatis的二级缓存。
二级缓存它是nameSpace(命名空间)级别的,前面介绍的Mapper接口开发可知,每一个Mapper的命名空间都是不同的,否则会报错。默认是关闭的,需要在配置文件中打开才可以使用。 (1)在MyBatis主配置文件中指定cacheEnabled属性值为true。
<settings> <setting name=ncacheEnabled" value="true"/> </settings>(2)在MyBatis Mapper配置文件中,配置缓存策略、缓存刷新频率、缓存的容量等属性,例 如:
<cache></cache> 什么都不配 就是默认配置(3)在配置Mapper时,通过useCache属性指定Mapper执行时是否使用缓存。另外,还可 以通过flushCache属性指定Mapper执行后是否刷新缓存,例如:
<select id=nlistAHUsern flushCache="false" useCache="true" resultType=ncom.blog4java.mybatis ,example ,entity.UserEntity" > select <include refid=,'userAllField',/> from user </select>上文在2.5提到过开启二级缓存的配置,也就是这个CachingExecutor组件,用到的装饰器模式,对其他几种Executor增加了二级缓存功能,可再返回2.5仔细看。 启用二级缓存后,我们查询数据库的顺序就为二级缓存>一级缓存>数据库。 前面我们也说到过一级缓存是sqlsession级别的,而且多个sqlsession缓存不能够共享,二级缓存虽然是nameSpace级别的(也称作是Mapper级别的),而且其缓存区域是跨sqlsesson的,看上去是缓存共享的,但是还会存在脏数据的情况。二级缓存缺陷 这里引用的二级缓存缺陷,并不是无解的,使用cache-ref 就可以解决这种多个mapper的问题。 cache-ref 代表引用别的命名空间的Cache 配置,两个命名空间的操作使用的是同一个Cache。在关联的表比较少,或者按照业务可以对表进行分组的时候可以使用。尽量对关联表的查询,关联的所有表的操作都能在同一个namespace。 一般来说,数据量大的表,需要用缓存的,这种恰恰不止一个mapper对其操作,业务逻辑复杂的,就会多个mapper对其操作,如果使用cache-ref ,声明多个mapper,那么缓存几乎失去了作用。这也是一开始讲Mybatis缺点提到缓存机制的原因。所以Mybatis提供了第三方的缓存接口,比如Redis。 这种第三方的引用,确实能为Mybatis加分不少。
因为都是为了提升系统性能,所以我把Mybatis的懒加载和缓存写在了同一章节下。懒加载机制,“懒”到恰好。在企业级开发中,一个普通页面的查询,就可能涉及多张表,比如我们将用户信息和订单信息,分俩个表,用户信息实体类包含订单信息实体类。在页面逻辑一开始只需要查询用户信息,而不需要订单信息时,我们可以使用懒加载,只加载用户信息,当需要get订单信息时,才回去加载订单信息。 官方解读:当通过 MyBatis配置开启懒加载机制时,执行查询操作不会触发关联的查询Mapper,而通过Getter方法 访问实体属性时才会执行一次关联的查询Mapper,然后为实体属性赋值。 直接上代码 配置文件: MyBatis主配置文件中提供了 lazyLoadingEnabled和aggressiveLazyLoading参数用来控制是否 开启懒加载机制。 lazyLoadingEnabled参数值为true时表示开启懒加载,否则表示不开启懒加载。 aggressiveLazyLoading参数用于控制ResultMap默认的加载行为,参数值为false表示ResultMap 默认的加载行为为懒加载,否则为积极加载。
引用
实体类User
public class User { //用户ID private int id; //用户姓名 private String username; //用户性别 private String sex; //订单信息 public OrderInfo orderInfo; //省略get、set方法 } @Test public void TestLazyQuery{ //原逻辑是要查用户信息和订单信息,但是页面并没有用到订单信息 //所以可以对订单信息使用懒加载,就是当需要get订单信息的时候,再去加载 User user=userMapper.getUserInfoById(1); //调用getOrderInfo()时执行懒加载 user.getOrderInfo(); }注意:想订单表关联用户表,sql只执行了一次,那么是无法使用懒加载的。因为它懒不起来,要分步才有懒的机会。比如在resultMap里写一个列的查询。具体过程 相关原理: 我们开启懒加载时,执行查询Mapper返回的实际上是通过Cglib或Javassist创建的动态代 理对象。假设我们指定了使用Cglig创建动态代理对象,调用动态代理对象的Getter方法时会执行 MyBatis中定义的拦截逻辑,在拦截方法中,通过Getter方法名称获取Java实体属性名称,在调用查询,使用查询结果为懒加载属性赋值。
这里单独写一个章节来总结Mybatis的设计模式,是因为设计思想是相当重要的。知其然 更要知其所以然。
设计模式总的来说有23种,但是细分后有的说24种,也有的说26种。这里我们暂时只能细分24种。6创7结11行。 除了设计模式,一些原则也是必须掌握,总共有七大原则。这里只介绍四种。
开闭原则:软件实体应该对外扩展,而修改应该关闭。翻译:功能可以延续扩展,但是不能修改我本身的代码。有点装饰模式的味道。
单一职责原则:这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。翻译:一个对象不要担任太多职责,尽量解耦,提高内聚性。
迪米特法则:只与你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。翻译:做好自己的事,不和不认识的人交流,不和不相关的事物发生关系。这和单一职责原则作用几乎一样,都是解耦,提高相对独立性。
接口隔离原则:2002 年罗伯特·C.马丁给“接口隔离原则”的定义是:客户端不应该被迫依赖于它不使用的方法(Clients should not be forced to depend on methods they do not use)。该原则还有另外一个定义:一个类对另一个类的依赖应该建立在最小的接口上(The dependency of one class to another one should depend on the smallest possible interface)。翻译:接口尽量精简,不需要实现的方法就不要写,另外不要把所有方法写到一个接口上,然后很多个类去实现它,应该为各个类所需的建立他们所需的专业的接口。
下面按流程一步步总结用到哪些设计模式,顺便介绍对应的设计模式。
定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
//2.获取SqlSession工厂对象 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);使用build构建出SqlSessionFactory对象。
工厂模式:定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。
//3.通过工厂对象获取SqlSession对象 sqlSession = sqlSessionFactory.openSession(true);这里是根据参数不同返回不同的对象。
工厂模式有简单工厂模式、工厂方法模式以及抽象工厂模式。三种工厂模式
定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。
//4.获取StudentMapper接口的实现类对象 StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); // StudentMapper mapper = new StudentMapperImpl();在Mapper接口开发中,我们写的是个接口,表面上看省略了DaoImpl类,实际MyBatis内部通过MapperProxy类实现InvocationHandler接口,重写了invoke方法,对接口实行了JDK动态代理。(代理模式分为动态代理和静态代理,常用的动态代理又分JDK代理和Cglib代理,关于代理模式上篇博客有详细介绍。)
定义:外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供一个一致的接口,而使这些子系统更加容易被访问的模式。该模式对外有一个统一接口,外部应用程序不用关心内部子系统的具体细节,这样会大大降低应用程序的复杂度,提高了程序的可维护性。 前面有提到过,sqlsession是面向客户的API,但实际操作的确实Executor,这就是典型的外观模式。
定义:指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式,它属于对象结构型模式。 这是在2.5 Executor组件解析有明确讲解。
定义:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。这和工厂模式有些类似,但是工厂模式是针对创造对象,而模板方法模式,是针对于步骤。 这个也是在2.5 Executor组件解析有明确讲解。
定义:指一个类只有一个实例,且该类能自行创建这个实例的来供外界访问的一种模式。
在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。 单例模式有懒汉式和饿汉式,详情
这里基本上都是在以上文章中没有提到过的,进行补充。
对查询结果进行分页,也可以通过拦截sql,重写sql进行分页。前者是全部查询出来后,一页一页展示,后者拦截查全部的sql变成是一页一页查。
例如:原sql:select * from table,拦截 后重写为:select t.* from (select * from table)t limit 0,15
插件运行原理和拦截器原理基本一样,插件拦截mybatis 四大接口Executor(内部dql执行器), StatementHandle(JDBC封装层), ResultSetHandler(返回结果集), ParameterHandler(参数处理器)。Mybatis 通过动态代理,为需要拦截的接口生成代理对象以实现接口方法拦截功能,每当执行这 4 种接口对象的方法时,就会进入拦截方法,具体就是InvocationHandler 的 invoke()方法。
Mybatis面试大全