Mybatis框架复习大纲【面试+提高】
1.MyBatis面试题汇总
1.1 JDBC编程有哪些不足之处,MyBatis是如何解决这些问题的?
① 数据库链接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库链接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据链接池,使用连接池管理数据库链接。
② Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
③ 向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决: Mybatis自动将java对象映射至sql语句。
④ 对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象。
1.2 MyBatis编程步骤是什么样的?
① 创建SqlSessionFactory
② 通过SqlSessionFactory创建SqlSession
③ 通过sqlsession执行数据库操作
④ 调用session.commit()提交事务
⑤ 调用session.close()关闭会话
1.3 MyBatis与Hibernate有哪些不同?
1.Mybatis和hibernate不同,它不完全是一个ORM框架,因为MyBatis需要程序员自己编写Sql语句。mybatis可以通过XML或注解方式灵活配置要运行的sql语句,并将java对象和sql语句映射生成最终执行的sql,最后将sql执行的结果再映射生成java对象。
2.Mybatis学习门槛低,简单易学,程序员直接编写原生态sql,可严格控制sql执行性能,灵活度高,非常适合对关系数据模型要求不高的软件开发,例如互联网软件、企业运营类软件等,因为这类软件需求变化频繁,一但需求变化要求成果输出迅速。但是灵活的前提是mybatis无法做到数据库无关性,如果需要实现支持多种数据库的软件则需要自定义多套sql映射文件,工作量大。
3.Hibernate对象/关系映射能力强,数据库无关性好,对于关系模型要求高的软件(例如需求固定的定制化软件)如果用hibernate开发可以节省很多代码,提高效率。但是Hibernate的学习门槛高,要精通门槛更高,而且怎么设计O/R映射,在性能和对象模型之间如何权衡,以及怎样用好Hibernate需要具有很强的经验和能力才行。
总之,按照用户的需求在有限的资源环境下只要能做出维护性、扩展性良好的软件架构都是好架构,所以框架只有适合才是最好。
1.4 使用MyBatis的mapper接口调用时有哪些要求?
① Mapper接口方法名和mapper.xml中定义的每个sql的id相同
② Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同
③ Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
④ Mapper.xml文件中的namespace即是mapper接口的类路径。
1.5 SqlMapConfig.xml中配置有哪些内容?
SqlMapConfig.xml中配置的内容和顺序如下:
properties(属性)
settings(配置)
typeAliases(类型别名)
typeHandlers(类型处理器)
objectFactory(对象工厂)
plugins(插件)
environments(环境集合属性对象)
environment(环境子属性对象)
transactionManager(事务管理)
dataSource(数据源)
mappers(映射器)
1.6简单的说一下MyBatis的一级缓存和二级缓存?
Mybatis首先去缓存中查询结果集,如果没有则查询数据库,如果有则从缓存取出返回结果集就不走数据库。Mybatis内部存储缓存使用一个HashMap,key为hashCode+sqlId+Sql语句。value为从查询出来映射生成的java对象
Mybatis的二级缓存即查询缓存,它的作用域是一个mapper的namespace,即在同一个namespace中查询sql可以从缓存中获取数据。二级缓存是可以跨SqlSession的。
1.7Mapper编写有哪几种方式?
①接口实现类继承SqlSessionDaoSupport
使用此种方法需要编写mapper接口,mapper接口实现类、mapper.xml文件
1).在sqlMapConfig.xml中配置mapper.xml的位置
2).定义mapper接口
3).实现类集成SqlSessionDaoSupport
mapper方法中可以this.getSqlSession()进行数据增删改查。
4).spring 配置
②使用org.mybatis.spring.mapper.MapperFactoryBean
1).在sqlMapConfig.xml中配置mapper.xml的位置
如果mapper.xml和mappre接口的名称相同且在同一个目录,这里可以不用配置
2).定义mapper接口
注意:
1、mapper.xml中的namespace为mapper接口的地址
2、mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致
3、 Spring中定义
③使用mapper扫描器
1).mapper.xml文件编写,
注意:
mapper.xml中的namespace为mapper接口的地址
mapper接口中的方法名和mapper.xml中的定义的statement的id保持一致
如果将mapper.xml和mapper接口的名称保持一致则不用在sqlMapConfig.xml中进行配置
2).定义mapper接口
注意mapper.xml的文件名和mapper的接口名称保持一致,且放在同一个目录
3).配置mapper扫描器
4).使用扫描器后从spring容器中获取mapper的实现对象
扫描器将接口通过代理方法生成实现对象,要spring容器中自动注册,名称为mapper 接口的名称。
2. 建立工程时选择建立java工程还是web工程?
当建立的工程,需要用到request/response时,需要建立web工程,否则Java工程即可。
3. MyBatis介绍
MyBatis本是apache公司一个名叫iBatis的开源项目,在2010年Apache将其转移给了Google公司,从apache software foundation 迁移到了google code,并且改名为MyBatis,后来在2013年11月又被Google将其放到了Github上。
MyBatis是一个优秀的持久层框架,它对jdbc操作数据库的过程进行了封装,开发者只需要关注SQL本身,而不需要浪费精力去处理,例如:注册驱动,创建connection,创建statement,手动设置参数,结果集检索等Jdbc繁杂的过程代码。
MyBatis通过xml或注解的方式将要执行的各种statement(statement、preparedStatemnt、CallableStatement)配置起来,并通过java对象和statement中的sql进行映射生成最终执行的sql语句,最后由mybatis框架执行sql并将结果映射成java对象并返回。
1).和jdbc比较:
mybatis抽离出数据库的连接,关闭的操作.抽离了sql语句,并且可以自动的进行参数的设置,封装结果集.
2).和hibernate比较:
- 性能:mybatis较hibernate高
- sql灵活性:mybatis较hibernate高
- 配置文件:mybatis较hibernate多(维护困难)
- 数据库的无关性:mybatis较hibernate低
4. jdbc编程步骤回顾
注册数据库驱动
创建并获取数据库链接
创建jdbc statement对象
设置sql语句
设置sql语句中的参数(使用preparedStatement)
通过statement执行sql并获取结果
对sql执行结果进行解析处理, while(resultSet.next)
释放资源(resultSet,preparedStatement,connection)
5. jdbc问题总结
数据库连接创建、释放频繁造成系统资源浪费,从而影响系统性能。如果使用数据库连接池可解决此问题。
Sql语句在代码中硬编码,造成代码不易维护,实际应用中sql变化的可能较大,sql变动需要改变java代码。
使用preparedStatement向占位符号传参数存在硬编码,因为sql语句的where条件不一定,可能多也可能少,修改sql还要修改代码,系统不易维护。
对结果集解析存在硬编码(查询列名),sql变化导致解析代码变化,系统不易维护,如果能将数据库记录封装成pojo(POJO是指简单的Java对象,实际就是普通JavaBeans,是为了避免和EJB混淆所创造的简称。POJO通指没有使用Entity Beans的普通java对象,可以把POJO作为支持业务逻辑的协助类。)对象解析比较方便。
6. MyBatis架构
Configuration MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中
SqlSession 作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能
Executor MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护
StatementHandler 封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等
ParameterHandler 负责对用户传递的参数转换成JDBC Statement 所对应的数据类型
ResultSetHandler 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合
TypeHandler 负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换
MappedStatement MappedStatement维护一条<select|update|delete|insert>节点的封装
SqlSource 负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql 表示动态生成的SQL语句以及相应的参数信息
7. MyBatis配置(第一种总结方式)
1).SqlMapConfig.xml,此文件作为mybatis的全局(核心)配置文件,配置了mybatis的运行环境等信息。
mapper.xml 文件即sql映射文件,文件中配置了操作数据库的sql语句。此文件需要在SqlMapConfig.xml中加载。 例如:User.xml
2).通过mybatis环境等配置信息构造SqlSessionFactory即会话工厂 例如:通过流的形式构造SqlSessionFactory会话工厂
①InputStream inputStream = Resources.getResourceAsStream(sqlMapConfig.xml路径位置);
②SqlSessionFactory sqlSessionFactory = new sqlSessionFactoryBuilder().build(inputStream);
3).由会话工厂创建sqlSession即会话,操作数据库需要通过sqlSession进行。 sqlSession是线程不安全的,每个线程都应该有它独自的sqlSession,使用完就关闭。
4).mybatis底层自定义了Executor执行器接口操作数据库,Executor接口有两个实现,一个是基本执行器、一个是缓存执行器。 (Executor才是真正操作数据库的,不过是底层 所以我们认为是sqlSession在进行操作)
5).Mapped Statement也是mybatis一个底层封装对象,它包装了mybatis配置信息及sql映射信息等。mapper.xml文件中一个sql对应一个Mapped Statement对象,sql的id即是Mapped statement的id。
6).Mapped Statement对sql执行输入参数进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql前将输入的java对象映射至sql中,输入参数映射就是jdbc编程中对preparedStatement设置参数。
7).Mapped Statement对sql执行输出结果进行定义,包括HashMap、基本类型、pojo,Executor通过Mapped Statement在执行sql后将输出结果映射至java对象中,输出结果映射过程相当于jdbc编程中对结果的解析处理过程。
8.Dao开发(第一种总结方式)
8-1.传统dao开发
8-1.1.使用上面的UserMapper.xml和SqlMapConfig.xml
8-1.2.创建Dao接口 方法名要与UserMapper.xml中的sql的id一致
8-1.3.创建Dao实现类
8-1.4.对Dao进行junit测试
原始Dao开发中存在以下问题:
Dao方法体存在重复代码:通过SqlSessionFactory创建SqlSession,调用SqlSession的数据库操作方法
调用sqlSession的数据库操作方法需要指定statement的id,这里存在硬编码,不便于开发维护。
8-2.Mapper动态代理开发
Mapper接口开发方法只需要程序员编写Mapper接口(相当于Dao接口),由Mybatis框架根据接口定义创建接口的动态代理对象,代理对象的方法体同上边Dao接口实现类方法。
Mapper接口开发需要遵循以下规范:
1).Mapper.xml文件中的namespace与mapper接口的类路径相同。
2).Mapper接口方法名和Mapper.xml中定义的每个statement的id相同
3).Mapper接口方法的输入参数类型和mapper.xml中定义的每个sql 的parameterType的类型相同
4).Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
8-2.1.SqlMapConfig.xml
8-2.2.UserMapper.java 接口
8-2.3.UserMapper.xml
8-2.4.使用junit进行测试
9.MyBatis配置(第二种总结方式)
全局配置文件:sqlmapconfig.xml
文件中的配置项是有顺序的,按照官方图来配.
configuration
- properties
- settings
- typeAliases
- typeHandlers
- objectFactory
- plugins
- environments
* envioronment
* transactionManager
* dataSource
- databaseIdProvider
- mappers
例如:
settings: 全局参数设置
设置延迟加载:
typeAliases:类型别名
mappers:
映射文件
注意点
#{xxx}
表示一个占位符,jdbc中的?通过#{xxx} 可以将外部传递过来映射到sql语句中,可以有效的防止sql注入.
xxx表示一个sql串的拼接,不可以有效防止sql注入.如果是xxx表示一个sql串的拼接,不可以有效防止sql注入.如果是{xxx},输入的参数类型是简单类型,那么${xxx}中的xxx必须是value.
parameterType:表示输入参数的数据类型
resultType:输出结果的数据类型,如果查询的是列表,那么resultType只需要设置列表中的元素的数据类即可.
核心API
执行流程:
加载sqlmapconfig.xml,通过sqlsessionfactorybuilder,构建sqlsesionfactroy对象.由它构建sqlsession提供增删改查等操作数据库的方法.
SqlSessionFactoryBuilder
案例
SqlSessionFactory
创建SqlSession的方法:
用openSession方法获取的SqlSession有以下特点:
- 开启事务,但是不会自动提交事务
- 将从配置文件中配置的数据源中获取连接(Connection)
- 事务级别默认使用驱动或数据源的
- 预编译不会被重用,不会批量更新
列举3个执行类型参数(ExecutorType):
- ExecutorType.SIMPLE 每次执行都预编译
- ExecutorType.REUSE 重用预编译
- ExecutorType.BATCH 批量预编译
SqlSession
SqlSession执行增删改查以及事务操作.
10.Dao开发(第二种总结方式)
实现方式:
1. 传统的Dao建一个dao 建立一个接口,再建实现类.
2. mapper代理的方式,只需要写接口,不需要写实现类,实现类由mybatis框架自动创建(官方推荐)
传统的Dao
sqlsessionfactorybuilder当作一个工具类,一旦使用完毕,就应该销毁,最佳使用范围在方法内部.
sqlsessionfactory要单例存在,一旦创建就应当在整个程序运行期使用,没必要创建一次.使用范围整个运行期.(整合spring时,可以由spring来管理)
sqlsession是多例的,它线程不安全的,也不能被共享的,使用范围是在方法的内部.而且,一旦使用完成,必须要关闭,在finally中关闭.
接口:
实现类:
优化:
可以建立一个BaseDaoImpl 继承sqlsesiondaosupport ,在此进行sqlsessionfactory注入。
mapper动态代理的方式
mapper代理的方式,只需要写接口,不需要写实现类,实现类由mybatis框架自动创建.
需要遵守规则:
1. sql的映射文件中的namespace要和Mapper接口中的类路径(全限定名)一致
2. sql的映射文件中的sql的id要和mapper接口中的方法的名称一致
3. sql的映射文件中的parameterType要和mapper接口中的方法的参数类型一致
4. sql的映射文件中的resultType要和mapper接口中的方法的返回值数据类型一致
11. #{} 与 ${}区别
#{}表示一个占位符号,通过#{}可以实现preparedStatement向占位符中设置值,自动进行java类型和jdbc类型转换。#{}可以有效防止sql注入。 #{}可以接收简单类型值或pojo属性值。 如果parameterType传输单个简单类型值,#{}括号中可以是value或其它名称。
${}表示拼接sql串,通过${}可以将parameterType 传入的内容拼接在sql中且不进行jdbc类型转换, ${}可以接收简单类型值或pojo属性值,如果parameterType传输单个简单类型值,${}括号中只能是value。
sql注入案例:"%"#{}"%" 如果#{}传入的是<!-- 如果是字符串拼接${}会直接导致误认为注释没完成导致出错。
12.parameterType和resultType
parameterType:指定输入参数类型,mybatis通过ognl从输入对象中获取参数值拼接在sql中。
resultType:指定输出结果类型,mybatis将sql查询结果的一行记录数据映射为resultType指定类型的对象。如果有多条数据,则分别进行映射,并把对象放到容器List中
13.selectOne和selectList
selectOne查询一条记录,如果使用selectOne查询多条记录则抛出异常:
org.apache.ibatis.exceptions.TooManyResultsException: Expected one result (or null) to be returned by selectOne(), but found: 3
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:70)
selectList可以查询一条或多条记录。
14.Mybatis解决jdbc编程的问题
1).数据库连接创建、释放频繁造成系统资源浪费从而影响系统性能,如果使用数据库连接池可解决此问题。
解决:在SqlMapConfig.xml中配置数据连接池,使用连接池管理数据库链接。
2).Sql语句写在代码中造成代码不易维护,实际应用sql变化的可能较大,sql变动需要改变java代码。
解决:将Sql语句配置在XXXXmapper.xml文件中与java代码分离。
3).向sql语句传参数麻烦,因为sql语句的where条件不一定,可能多也可能少,占位符需要和参数一一对应。
解决:Mybatis自动将java对象映射至sql语句,通过statement中的parameterType定义输入参数的类型。
4).对结果集解析麻烦,sql变化导致解析代码变化,且解析前需要遍历,如果能将数据库记录封装成pojo对象解析比较方便。
解决:Mybatis自动将sql执行结果映射至java对象,通过statement中的resultType定义输出结果的类型。
15.分页查询
ResultHandler参数:可以把每行记录包装成其他数据类型,如:List,Map,Set.只需要写个接口.
16.控制事务
17.缓存
一级缓存指的就是sqlsession,在sqlsession中有一个数据区域,是map结构,这个区域就是一级缓存区域。一级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。一级缓存中的value,就是查询出的结果对象。(本地缓存)
二级缓存指的就是同一个namespace下的mapper,二级缓存中,也有一个map结构,这个区域就是一级缓存区域。一级缓存中的key是由sql语句、条件、statement等信息组成一个唯一值。一级缓存中的value,就是查询出的结果对象。(自定义缓存)
一级缓存是默认使用的。
二级缓存需要手动开启。
二级缓存的配置
1. 开启二级缓存中开关
2. 在mapper映射文件中开启二级缓存
3. 禁止使用缓存
useCache=”false”
4. 刷新缓存
select语句默认是false,增删改语句默认是true
18.Mappers
Mapper是个接口,里面定义方法匹配SqlSession中方法.
Mapper注解,参考Mybatis官方文档.
示例:
19.动态sql
MyBatis使用OGNL表达式,来支持一些常用的标签.
- if
- choose(when,otherwise)
- trim(where,set)
- foreach
案例:
if
choose, when, otherwise
trim, where, set
foreach
bind
创建一个ognl表达式,用于语句中.
Multi-db vendor support
Pluggable Scripting Languages For Dynamic SQL
为动态sql定制可插拔脚本语句
使用步骤:
实现LanguageDriver接口
再mybatis-config.xml或sqlmapconfig.xml文件中覆盖默认别名,使用自定义
在映射文件中使用
也可以在mapper中使用
sql片段
抽取sql重复代码,提供效率。
20.多表查询
输出结果类型
resultType,字段和java对象中属性一致推荐使用resultType
resultMap,字段和java对象中属性不一致的情况比较多的时候使用resultMap
ResultMap
resultType使用时,设置值时需要查询的列名和java对象属性名一致.如果不一致的时候,可以使用resultMap.使用时需要先定义,再使用.
resultMap做复杂的映射配置(多表查询).
映射一对一关系
映射一对多关系
多对多映射
21.mybatis和spring的整合
整合的步骤
1). 创建工程
2). 加入jar spring的包 mybatis的包 依赖包 数据库驱动 数据连接池 整合包 日志包
3). mybatis配置文件
4). spring的配置文件
5). 数据库、日志的配置文件
db.properties
log4j.properties
MyBatis使用中几点经验
1.手动增量配置映射文件
当有工具生成Mapper等配置文件的时候,很多人就不愿意手动写了。其实MyBatis的生成工具不是特别有用,生成的方法几乎不可用,删删改改老半天还不如自己手写快。而且需要新加或修改属性、方法时,也是没法使用生成的文件,因为需要保留好原有的一些属性和方法。手写映射文件时先定义出用到的字段,这样配置文件会简洁清晰,同时结果映射时效率会更高。
2.Mapper层参数为Map,由Service层负责重载。
Mapper由于机制的问题,不能重载,参数一般设置成Map,但这样会使参数变得模糊,如果想要使代码变得清晰,可以通过service层来实 现重载的目的,对外提供的Service层是重载的,但这些重载的Service方法其实是调同一个Mapper,只不过相应的参数并不一致。
也许有人会想,为什么不在Service层也设置成Map呢?我个人是不推荐这么做的,虽然为了方便,我在之前的项目中也大量采用了这种方式,但 很明显会给日后的维护工作带来麻烦。因为这么做会使你整个MVC都依赖于Map模型,这个模型其实是很不错的,方便搭框架,但存在一个问题:仅仅看方法签 名,你不清楚Map中所拥有的参数个数、类型、每个参数代表的含义。
试想,你只对Service层变更,或者DAO层变更,你需要清楚整个流程中Map传递过来的参数,除非你注释或者文档良好,否则必须把每一层的 代码都了解清楚,你才知道传递了哪些参数。针对于简单MVC,那倒也还好,但如果层次复杂之后,代码会变得异常复杂,而且如果我增加一个参数,需要把每一 个层的注释都添加上。相对于注释,使用方法签名来保证这种代码可控性会来得更可行一些,因为注释有可能是过时的,但方法签名一般不太可能是陈旧的。
3.尽量少用if choose等语句,降低维护的难度。
Mybatis的配置SQL时,尽量少用if choose 等标签,能用SQL实现判断的尽量用SQL来判断(CASE WHEN ,DECODE等),以便后期维护。否则,一旦SQL膨胀,超级恶心,如果需要调试Mybatis中的SQL,需要去除大量的判断语句,非常麻烦。另一方 面,大量的if判断,会使生成的SQL中包含大量的空格,增加网络传输的时间,也不可取。
而且大量的if choose语句,不可避免地,每次生成的SQL会不太一致,会导致ORACLE大量的硬解析,也不可取。
我们来看看这样的SQL
Xml代码
这样的if判断,其实是完全没有必要的,我们可以很简单的采用DECODE来解决默认值问题:
Xml代码
当然有人会想,引入CASE WHEN,DECODE会导致需要ORACLE函数解析,会拖慢SQL执行时间,有兴趣的同学可以回去做一下测试,看看是否会有大的影响。就个人经验而 言,在我的开发过程,没有发现因为函数解析导致SQL变慢的情形。影响SQL执行效率的一般情况下是JOIN、ORDER BY、DISTINCT、PARTITATION BY等这些操作,这些操作一般与表结构设计有很大的关联。相对于这些的效率影响程度,函数解析对于SQL执行速度影响应该是可以忽略不计的。
另外一点,对于一些默认值的赋值,像上面那条SQL,默认成当前日期什么的,其实可以完全提到Service层或Controller层做处理,在Mybatis中应该要少用这些判断。因为,这样的话,很难做缓存处理。如果startdate为空,在SQL上使用动态的SYSDATE,就无法确定缓存startdate日期的key应该是什么了。所以参数最好在传递至Mybatis之前都处理好,这样Mybatis层也能减少部分if choose语句,同时也方便做缓存处理。
当然不使用if choose也并不是绝对的,有时候为了优化SQL,不得不使用if来解决,比如说LIKE语句,当然一般不推荐使用LIKE,但如果存在使用的场景,尽 可能在不需要使用时候去除LIKE,比如查询文章标题,以提高查询效率。 最好的方式是使用lucence等搜索引擎来解决这种全文索引的问题。
总的来说,if与choose判断分支是不可能完全去除的,但是推荐使用SQL原生的方式来解决一些动态问题,而不应该完全依赖Mybatis来完成动态分支的判断,因为判断分支过于复杂,而且难以维护。
4.用XML注释取代SQL注释。
Mybatis中原SQL的注释尽量不要保留,注释会引发一些问题,如果需要使用注释,可以在XML中用来注释,保证在生成的SQL中不会存在SQL注释,从而降低问题出现的可能性。这样做还有一个好处,就是在IDE中可以很清楚的区分注释与 SQL。
现在来谈谈注释引发的问题,我做的一个项目中,分页组件是基于Mybatis的,它会在你写的SQL脚本外面再套一层SELECT COUNT(*) ROWNUM_ FROM (....) 计算总记录数,同时有另一个嵌套
Xml代码
即使传入的参数中存在对应的参数,实际也不会产生效果,因为后面的内容实际上是被完全注释了。这种错误,如果不经过严格的测试,是很难发现的。一般情况下,XML注释完全可以替代SQL注释,因此这种行为应该可以禁止掉。
5.尽可能使用#{},而不是${}.
Mybatis中尽量不要使用${},尽量这样做很方便开发,但是有一个问题,就是大量使用会导致ORACLE的硬解析,拖慢数据库性能,运行越久,数据库性能会越差。对于一般多个字符串IN的处理,可以参考文章最后的解决方案基本可以解决大部分${}.
关于${},另一个误用的地方就是LIKE,我这边还有个案例:比如一些树型菜单,节点会设计成'01','0101',用两位节点来区分层级,这时候, 如果需要查询01节点下所有的节点,最简单的SQL便是:SELECT * FROM TREE WHERE ID LIKE '01%',这种SQL其实无可厚非,因为它也能用到索引,所以不需要特别的处理,直接使用就行了。但如果是文章标题,则需要额外注意了:SELECT * FROM T_NEWS_TEXT WHERE TITLE LIKE '%OSC%',这是怎么也不会用到索引的,上面说了,最好采用全文检索。但如果离不开LIKE,就需要注意使用的方式: ID LIKE #{ID} || '%'而不是ID LIKE '${ID}%',减少硬解析的可能。
有人觉得使用||会增加ORACLE处理的时间,我觉得不要把ORACLE看得太傻,虽然有时候确实非常傻,有空可以再总结ORACLE傻不垃圾的地方,但是稍加测试便知:这种串联方式,对于整个SQL的解析执行,应该是微乎其微的。
当然还有一些特殊情况是没有办法处理的,比如说动态注入列名、表名等。对于这些情况,则比较棘手,没有找到比较方便的手段。由于这种情况出现的可 能性会比较少,所以使用${}倒也不至于有什么太大的影响。当然你如果有代码洁癖的话,可以使用ORACLE的动态执行SQL的机制Execute immediate,这样就可以完全避免${}出现的可能性了。这样会引入比较复杂的模型,这个时候,你就需要取舍了。
针对于以上动态SQL所导致的问题,最激进的方式是全部采用存储过程,用数据库原生的方式来解决,方便开发调试,当然也会带来问题:对开发人员会有更高的要求、存储过程的管理等等,我这边项目没有采用过这种方式,这里不做更多的展开。
Mybatis中设计模式:
Mybatis至少遇到了以下的设计模式的使用:
Builder模式,例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
工厂模式,例如SqlSessionFactory、ObjectFactory、MapperProxyFactory;
单例模式,例如ErrorContext和LogFactory;
代理模式,Mybatis实现的核心,比如MapperProxy、ConnectionLogger,用的jdk的动态代理;还有executor.loader包使用了cglib或者javassist达到延迟加载的效果;
组合模式,例如SqlNode和各个子类ChooseSqlNode等;
模板方法模式,例如BaseExecutor和SimpleExecutor,还有BaseTypeHandler和所有的子类例如IntegerTypeHandler;
适配器模式,例如Log的Mybatis接口和它对jdbc、log4j等各种日志框架的适配实现;
装饰者模式,例如Cache包中的cache.decorators子包中等各个装饰者的实现;
迭代器模式,例如迭代器模式PropertyTokenizer;
接下来挨个模式进行解读,先介绍模式自身的知识,然后解读在Mybatis中怎样应用了该模式。
1、Builder模式
Builder模式的定义是“将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式,一般来说,如果一个对象的构建比较复杂,超出了构造函数所能包含的范围,就可以使用工厂模式和Builder模式,相对于工厂模式会产出一个完整的产品,Builder应用于更加复杂的对象的构建,甚至只会构建产品的一个部分。
在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调用XMLConfigBuilder读取所有的MybatisMapConfig.xml和所有的*Mapper.xml文件,构建Mybatis运行的核心对象Configuration对象,然后将该Configuration对象作为参数构建一个SqlSessionFactory对象。
其中XMLConfigBuilder在构建Configuration对象时,也会调用XMLMapperBuilder用于读取*Mapper文件,而XMLMapperBuilder会使用XMLStatementBuilder来读取和build所有的SQL语句。
在这个过程中,有一个相似的特点,就是这些Builder会读取文件或者配置,然后做大量的XpathParser解析、配置或语法的解析、反射生成对象、存入结果缓存等步骤,这么多的工作都不是一个构造函数所能包括的,因此大量采用了Builder模式来解决。
对于builder的具体类,方法都大都用build*开头,比如SqlSessionFactoryBuilder为例,它包含以下方法:
即根据不同的输入参数来构建SqlSessionFactory这个工厂对象。
2、工厂模式
在Mybatis中比如SqlSessionFactory使用的是工厂模式,该工厂没有那么复杂的逻辑,是一个简单工厂模式。
简单工厂模式(Simple Factory Pattern):又称为静态工厂方法(Static Factory Method)模式,它属于类创建型模式。在简单工厂模式中,可以根据参数的不同返回不同类的实例。简单工厂模式专门定义一个类来负责创建其他类的实例,被创建的实例通常都具有共同的父类。
SqlSession可以认为是一个Mybatis工作的核心的接口,通过这个接口可以执行执行SQL语句、获取Mappers、管理事务。类似于连接MySQL的Connection对象。
可以看到,该Factory的openSession方法重载了很多个,分别支持autoCommit、Executor、Transaction等参数的输入,来构建核心的SqlSession对象。
在DefaultSqlSessionFactory的默认工厂实现里,有一个方法可以看出工厂怎么产出一个产品:
这是一个openSession调用的底层方法,该方法先从configuration读取对应的环境配置,然后初始化TransactionFactory获得一个Transaction对象,然后通过Transaction获取一个Executor对象,最后通过configuration、Executor、是否autoCommit三个参数构建了SqlSession。
在这里其实也可以看到端倪,SqlSession的执行,其实是委托给对应的Executor来进行的。
而对于LogFactory,它的实现代码:
这里有个特别的地方,是Log变量的的类型是Constructor<? extends Log>,也就是说该工厂生产的不只是一个产品,而是具有Log公共接口的一系列产品,比如Log4jImpl、Slf4jImpl等很多具体的Log。
3、单例模式
单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。
单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。
在Mybatis中有两个地方用到单例模式,ErrorContext和LogFactory,其中ErrorContext是用在每个线程范围内的单例,用于记录该线程的执行环境错误信息,而LogFactory则是提供给整个Mybatis使用的日志工厂,用于获得针对项目配置好的日志对象。
ErrorContext的单例实现代码:
构造函数是private修饰,具有一个static的局部instance变量和一个获取instance变量的方法,在获取实例的方法中,先判断是否为空如果是的话就先创建,然后返回构造好的对象。
只是这里有个有趣的地方是,LOCAL的静态实例变量使用了ThreadLocal修饰,也就是说它属于每个线程各自的数据,而在instance()方法中,先获取本线程的该实例,如果没有就创建该线程独有的ErrorContext。
4、代理模式
代理模式可以认为是Mybatis的核心使用的模式,正是由于这个模式,我们只需要编写Mapper.java接口,不需要实现,由Mybatis后台帮我们完成具体SQL的执行。
代理模式(Proxy Pattern) :给某一个对象提供一个代 理,并由代理对象控制对原对象的引用。代理模式的英 文叫做Proxy或Surrogate,它是一种对象结构型模式。
代理模式包含如下角色:
Subject: 抽象主题角色
Proxy: 代理主题角色
RealSubject: 真实主题角色
这里有两个步骤,第一个是提前创建一个Proxy,第二个是使用的时候会自动请求Proxy,然后由Proxy来执行具体事务;
当我们使用Configuration的getMapper方法时,会调用mapperRegistry.getMapper方法,而该方法又会调用mapperProxyFactory.newInstance(sqlSession)来生成一个具体的代理:
在这里,先通过T newInstance(SqlSession sqlSession)方法会得到一个MapperProxy对象,然后调用T newInstance(MapperProxy<T> mapperProxy)生成代理对象然后返回。
而查看MapperProxy的代码,可以看到如下内容:
非常典型的,该MapperProxy类实现了InvocationHandler接口,并且实现了该接口的invoke方法。
通过这种方式,我们只需要编写Mapper.java接口类,当真正执行一个Mapper接口的时候,就会转发给MapperProxy.invoke方法,而该方法则会调用后续的sqlSession.cud>executor.execute>prepareStatement等一系列方法,完成SQL的执行和返回。
5、组合模式
组合模式组合多个对象形成树形结构以表示“整体-部分”的结构层次。
组合模式对单个对象(叶子对象)和组合对象(组合对象)具有一致性,它将对象组织到树结构中,可以用来描述整体与部分的关系。同时它也模糊了简单元素(叶子对象)和复杂元素(容器对象)的概念,使得客户能够像处理简单元素一样来处理复杂元素,从而使客户程序能够与复杂元素的内部结构解耦。
在使用组合模式中需要注意一点也是组合模式最关键的地方:叶子对象和组合对象实现相同的接口。这就是组合模式能够将叶子节点和对象节点进行一致处理的原因。
Mybatis支持动态SQL的强大功能,比如下面的这个SQL:
在这里面使用到了trim、if等动态元素,可以根据条件来生成不同情况下的SQL;
在DynamicSqlSource.getBoundSql方法里,调用了rootSqlNode.apply(context)方法,apply方法是所有的动态节点都实现的接口:
对于实现该SqlSource接口的所有节点,就是整个组合模式树的各个节点:
组合模式的简单之处在于,所有的子节点都是同一类节点,可以递归的向下执行,比如对于TextSqlNode,因为它是最底层的叶子节点,所以直接将对应的内容append到SQL语句中:
但是对于IfSqlNode,就需要先做判断,如果判断通过,仍然会调用子元素的SqlNode,即contents.apply方法,实现递归的解析。
6、模板方法模式
模板方法模式是所有模式中最为常见的几个模式之一,是基于继承的代码复用的基本技术。
模板方法模式需要开发抽象类和具体子类的设计师之间的协作。一个设计师负责给出一个算法的轮廓和骨架,另一些设计师则负责给出这个算法的各个逻辑步骤。代表这些具体逻辑步骤的方法称做基本方法(primitive method);而将这些基本方法汇总起来的方法叫做模板方法(template method),这个设计模式的名字就是从此而来。
模板类定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
在Mybatis中,sqlSession的SQL执行,都是委托给Executor实现的,Executor包含以下结构:
其中的BaseExecutor就采用了模板方法模式,它实现了大部分的SQL执行逻辑,然后把以下几个方法交给子类定制化完成:
该模板方法类有几个子类的具体实现,使用了不同的策略:
简单SimpleExecutor:每执行一次update或select,就开启一个Statement对象,用完立刻关闭Statement对象。(可以是Statement或PrepareStatement对象)
重用ReuseExecutor:执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。(可以是Statement或PrepareStatement对象)
批量BatchExecutor:执行update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)
比如在SimpleExecutor中这样实现update方法:
7、适配器模式
适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。
在Mybatsi的logging包中,有一个Log接口:
该接口定义了Mybatis直接使用的日志方法,而Log接口具体由谁来实现呢?Mybatis提供了多种日志框架的实现,这些实现都匹配这个Log接口所定义的接口方法,最终实现了所有外部日志框架到Mybatis日志包的适配:
比如对于Log4jImpl的实现来说,该实现持有了org.apache.log4j.Logger的实例,然后所有的日志方法,均委托该实例来实现。
8、装饰者模式
装饰模式(Decorator Pattern) :动态地给一个对象增加一些额外的职责(Responsibility),就增加对象功能来说,装饰模式比生成子类实现更为灵活。其别名也可以称为包装器(Wrapper),与适配器模式的别名相同,但它们适用于不同的场合。根据翻译的不同,装饰模式也有人称之为“油漆工模式”,它是一种对象结构型模式。
在mybatis中,缓存的功能由根接口Cache(org.apache.ibatis.cache.Cache)定义。整个体系采用装饰器设计模式,数据存储和缓存的基本功能由PerpetualCache(org.apache.ibatis.cache.impl.PerpetualCache)永久缓存实现,然后通过一系列的装饰器来对PerpetualCache永久缓存进行缓存策略等方便的控制。如下图:
用于装饰PerpetualCache的标准装饰器共有8个(全部在org.apache.ibatis.cache.decorators包中):
FifoCache:先进先出算法,缓存回收策略
LoggingCache:输出缓存命中的日志信息
LruCache:最近最少使用算法,缓存回收策略
ScheduledCache:调度缓存,负责定时清空缓存
SerializedCache:缓存序列化和反序列化存储
SoftCache:基于软引用实现的缓存管理策略
SynchronizedCache:同步的缓存装饰器,用于防止多线程并发访问
WeakCache:基于弱引用实现的缓存管理策略
另外,还有一个特殊的装饰器TransactionalCache:事务性的缓存
正如大多数持久层框架一样,mybatis缓存同样分为一级缓存和二级缓存
一级缓存,又叫本地缓存,是PerpetualCache类型的永久缓存,保存在执行器中(BaseExecutor),而执行器又在SqlSession(DefaultSqlSession)中,所以一级缓存的生命周期与SqlSession是相同的。
二级缓存,又叫自定义缓存,实现了Cache接口的类都可以作为二级缓存,所以可配置如encache等的第三方缓存。二级缓存以namespace名称空间为其唯一标识,被保存在Configuration核心配置对象中。二级缓存对象的默认类型为PerpetualCache,如果配置的缓存是默认类型,则mybatis会根据配置自动追加一系列装饰器。
Cache对象之间的引用顺序为:SynchronizedCache–>LoggingCache–>SerializedCache–>ScheduledCache–>LruCache–>PerpetualCache
9、迭代器模式
迭代器(Iterator)模式,又叫做游标(Cursor)模式。GOF给出的定义为:提供一种方法访问一个容器(container)对象中各个元素,而又不需暴露该对象的内部细节。
Java的Iterator就是迭代器模式的接口,只要实现了该接口,就相当于应用了迭代器模式:
比如Mybatis的PropertyTokenizer是property包中的重量级类,该类会被reflection包中其他的类频繁的引用到。这个类实现了Iterator接口,在使用时经常被用到的是Iterator接口中的hasNext这个函数。
可以看到,这个类传入一个字符串到构造函数,然后提供了iterator方法对解析后的子串进行遍历,是一个很常用的方法类。
sql中带有IN的子查询绑定变量实现方式
在sql中经常会带有IN的子查询,如 where id in (1,2,3)。如果这样的语句在数据库中出现,将引起数据库的大量硬解析与共享池SQL碎片,
下面介绍种将这些In list给绑定起来:
1:首先针对数据类型和字符类型的绑定创建两个类型。create or replace type numtabletype as table of number;
create or replace type vartabletype as table of varchar2(1000);
2:创建两个函数,分别来解析In list中的串
CREATE OR REPLACE FUNCTION str2numList(p_string IN varchar2) RETURN numtabletype
AS
v_str LONG DEFAULT p_string || ',';
v_n NUMBER;
v_data numtabletype := numtabletype();
BEGIN
LOOP
v_n := to_number(instr(v_str, ','));
EXIT WHEN (nvl(v_n, 0) = 0);
v_data.extend;
v_data(v_data.count) := ltrim(rtrim(substr(v_str,1,v_n-1)));
v_str := substr(v_str, v_n+1);
END LOOP;
RETURN v_data;
END;
CREATE OR REPLACE FUNCTION str2varList(p_string IN varchar2) RETURN vartabletype
AS
v_str LONG DEFAULT p_string || ',';
v_n varchar2(2000);
v_data vartabletype := vartabletype();
BEGIN
LOOP
v_n := to_number(instr(v_str, ','));
EXIT WHEN (nvl(v_n, 0) = 0);
v_data.extend;
v_data(v_data.count) := ltrim(rtrim(substr(v_str,1,v_n-1)));
v_str := substr(v_str, v_n+1);
END LOOP;
RETURN v_data;
END;
3:使用方式添加Hint为了稳定执行计划
SELECT /*+ ordered user_nl(a,b) */ b.* FROM TABLE(str2numList(:bind0)) a, ACCOUNT b WHERE b.id=a.column_value;
SELECT /*+ ordered user_nl(a,b) */ b.* FROM TABLE(str2numList('1,2,3')) a, ACCOUNT b WHERE b.id=a.column_value;
SELECT /*+ leading(a) */ * FROM ACCOUNT WHERE ID IN (SELECT * FROM TABLE(str2numList(:bind1)) a);
SELECT /*+ leading(a) */ * FROM ACCOUNT WHERE ID