1.动态SQL
(1)if判断语句:test判断是否为空,通过toString()可以判断字符串是否相等;通过typeHandler可以判断枚举;
<select id="getLogs" resultMap="log">
select * from Log where start <![CDATA[ >= ]]> #{start} and start <![CDATA[ <= ]]> #{end}
<if test="keyword != null">
and (name like concat('%',#{keyword},'%') or desr like concat('%',#{keyword},'%'))
</if>
order by start desc limit #{offset},#{count}
</select>
如果keyword不为空,则采用模糊匹配查询;否则不用构造这个条件;
(2)choose、when、otherwise
使用类似if,只不过是多种情况判断
(3)trim、where、set
<select id="getLogs" resultMap="log">
select * from Log
<where>
<if test="keyword != null">
and (name like concat('%',#{keyword},'%') or desr like concat('%',#{keyword},'%'))
</if>
</where>
order by start desc limit #{offset},#{count}
</select>
<select id="getLogs" resultMap="log">
select * from Log
<trim prefix="where" prefixOverrides="and">
<if test="keyword != null">
and (name like concat('%',#{keyword},'%') or desr like concat('%',#{keyword},'%'))
</if>
</trim>
order by start desc limit #{offset},#{count}
</select>
where元素里面的条件成立时,才会加入where这个SQL关键字到组装的SQL中,会自动去掉多余and、or;
trim去掉一些特殊子字符串,示例指定的是and,效果与where一致;
set用法与where类似,用于更新数据,会自动去掉多余的逗号,也可以用trim替换,<trim prefix="SET" suffixOverrides=",">...</trim>
;
(4)foreach循环元素
支持数组、List、Set接口的集合;
<select id="findById" resultType="user">
select * from user where id in
<foreach item="id" index="index" collection="userIdList" open="(" separator="," close=",">
#{id}
</foreach>
</select>
collection表示传递进来的参数名,可以是数组、List、Set等;
item表示循环中的当前元素;
index表示当前元素在集合的位置下标;
open、close表示以什么符号将这写集合元素包装起来;
separator表示各个元素的间隔符;
注意:in语句会消耗大量性能,要注意使用;还有一些数据库会对SQL长度有限制,所以使用前要预估collection对象的长度;
(5)bind:通过OGNL表达式自定义一个上下文变量;可以兼容数据库语言;
<select id="findRole" parameterType="string" resultType="role">
<bind name="pattern" value="'%'+_parameter+'%'"/>
select ....where name like #{pattern}
</select>
_parameter代表传递进来的参数;
至此,MyBatis的应用已基本说明完整;以下两部分主要说明MyBatis的一些原理与底层设计,为了更加深刻理解;
2.MyBatis的解析和运行原理
(1)运行过程:
1)构建SqlSessionFactory:
-通过XMLConfigBuilder解析配置的XML文件,读取所配置的参数并存入Configuration类对象(单例);
-使用Configuration对象创建SqlSessionFactory(接口),MyBatis提供了默认的实现类DefaultSqlSessionFactory,一般不需要自定义;
如上的构建模式是Builder模式,因为对于复杂对象,使用构造参数很难实现,就使用一个类作为统领,一步步构建所需内容,然后再通过它构建最终对象,每一步都很清晰;
以typeHandler为例,XMLConfigBuilder 中会解析注册typeHandlers到TypeHandlerRegistry,从父类BaseBuilder 可知,TypeHandlerRegistry 是Configuration 的一个属性,因此我们可以从Configuration 单例拿到TypeHandlerRegistry 对象,进而拿到所注册的tyepHandler;其他的配置注册类似;
public class XMLConfigBuilder extends BaseBuilder {
private void parseConfiguration(XNode root) {
...
typeHandlerElement(root.evalNode("typeHandlers"));
...
}
}
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
从上例可知,Configuration是非常重要的一个单例,包含了MyBatis几乎所有的配置;它负责读入XML配置文件、初始化基础配置和方法、提供单例和参数;
XMLConfigBuilder解析XML时,一条保存SQL+相关配置包括三部分(映射器内部组成):
-MappedStatement:保存一个映射器节点的内容,是一个包括SQL,SQL id,缓存信息,resultMap,parameterType等信息的类;一般不需要修改;
-SqlSource:是MappedStatement的一个属性,是一个接口,主要是根据上下文和参数解析生成需要的SQL;有重要实现类DynamicSqlSource、ProviderSqlSource、RawSqlSource、StaticSqlSource;一般不需要修改;
-BoundSql:结果对象,就是SqlSource通过对SQL和参数的联合解析得到的SQL和参数;一般插件会通过BoundSql进而修改当前运行的SQL和参数,满足自己的需求;
BoundSql有三个主要属性:
-parameterObject:参数本身,可以传递简单对象(int,String等,int会被MyBatis变为Integer对象传递)、POJO、Map、@Param注解的参数(传递多个参数,MyBatis会把parameterObject变成一个Map<String,Object>,没有注解,键值关系按顺序规划,有注解,键值被置换成@Param注解键值);
-parameterMappings:是一个List,parameterMapping对象会描述属性名称、表达式、javaType等;一般不需要改变;作用是通过它可以实现参数与SQL结合,以便PreparedStatement能通过它找到parameterObject对象的属性设置参数;
-sql是在映射器中一条被SqlSource解析后的SQL,使用插件时,可按需修改;
String resource="mybatis-config.xml";
InputStream ins=Resource.getResourceAsStream(resource);
//MyBatis会根据文件流先生成Configuration对象,进而构建SqlSessionFactory对象
SqlSessionFactory factory=new SqlSessionactoryBuilder().build(ins);
2)运行过程
-映射器(Mapper)的动态代理
RoleMapper mapper=session.getMapper(RoleMapper.class);
获取Mapper接口对象,由源码可知,是启用MapperProxyFactory工厂来生成一个代理实例;从工厂源码可知,Mapper映射是通过动态代理来实现的,newInstance返回一个动态代理对象T:
@SuppressWarnings("unchecked")
protected T newInstance(MapperProxy<T> mapperProxy) {
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
}
public T newInstance(SqlSession sqlSession) {
final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
return newInstance(mapperProxy);
}
Mapper是一个JDK动态代理对象,就会执行invoke方法,Mapper作为一个接口,会执行cachedMapperMethod方法生成MapperMethod 对象,最后执行execute方法:
public class MapperProxy<T> implements InvocationHandler, Serializable {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
if (Object.class.equals(method.getDeclaringClass())) { //类
return method.invoke(this, args);
} else if (isDefaultMethod(method)) {
return invokeDefaultMethod(proxy, method, args);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
//MapperMethod是采用命令模式运行的类,包含了增删查改各种方法
final MapperMethod mapperMethod = cachedMapperMethod(method);
return mapperMethod.execute(sqlSession, args);
}
}
//通过查询MapperMethod 类的某个方法可知,最后是通过sqlSession对象去执行对象的SQL的;
private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
List<E> result;
Object param = method.convertArgsToSqlCommandParam(args);
if (method.hasRowBounds()) {
RowBounds rowBounds = method.extractRowBounds(args);
result = sqlSession.<E>selectList(command.getName(), param, rowBounds);
} else {
result = sqlSession.<E>selectList(command.getName(), param);
}
......
return result;
}
至此,可以理解MyBatis只用Mapper接口就可以运行了,因为Mapper的命名空间+方法名(SQL id),MyBatis就可以将其与代理对象(getMapper生成)绑定,通过动态代理运行;
3)SqlSession下的四大对象
-Executor:执行器,由它调度StatementHandler、ParameterHandler、ResultSetHandler等来执行对应的SQL;它是一个真正执行java与数据库交互的对象,MyBatis中有3种执行器:
–SIMPLE:简易执行器,默认配置;
–REUSE:能执行重用预处理语句的执行器;
–BATCH:执行重用语句和批量更新,批量专用执行器;
MyBatis会根据配置类型去确定需要创建哪一种Executor,缓存由CachingExecutor进行包装executor,运用插件如下:
interceptorChain.pluginAll(executor);
插件将构建一层层的动态代理对象,我们可以修改在调度真是的Executor方法之前执行配置插件的代码;
-StatementHandler:使用数据库的Statement(PreparedStatement)执行操作,许多重要插件都是通过拦截它来实现的;
RoutingStatementHandler分为3种:
–SimpleRoutingStatementHandler——Statement
–PreparedRoutingStatementHandler——PreparedStatement预编译处理
–CallableRoutingStatementHandler——CallableStatement存储过程处理
-ParameterHandler:处理SQL参数;就是完成对预编译参数的设置;从paramterObject对象中取得参数,使用注册好的typeHandler转换参数;这个接口包含两个方法,一个默认实现类DefaultParameterHandler:
public interface ParameterHandler {
Object getParameterObject(); //返回参数对象;
void setParameters(PreparedStatement ps) throws SQLException;//设置预编译SQL语句的参数;
}
-ResultSetHandler:进行数据集(ResultSet)的封装返回处理;
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;//包装结果集
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;//处理存储过程输出参数
}
总结:SqlSession是通过执行器Executor调度StatementHandler的prepare()进行预编译SQL和设置一些基本运行参数,然后用parameterize()方法启用ParameterHandler设置参数,完成预编译,执行查询,最后使用ResultSetHandler封装结果返回给调用者;
3.插件
插件就是在四大对象调度时插入我们的代码去执行一些特殊的需求;
1)插件接口
使用插件必须实现接口Interceptor:
public interface Interceptor {
//覆盖拦截对象原有方法,可以通过Invocation反射调度原来的方法
Object intercept(Invocation invocation) throws Throwable;
//target是被拦截对象,plugin是给被拦截对象生成一个代理对象
Object plugin(Object target);
//允许在plugin元素中配置所需参数
void setProperties(Properties properties);
}
由前文XMLConfigBuilder源码可知,插件的初始化是在MyBatis初始化时,就开始读入插件节点和配置的参数,使用反射技术生成对应的插件实例,并保存到List集合,以便后续读取与使用;
前文通过interceptorChain.pluginAll(executor)定义插件,具体看下pluginAll:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
调用plugin方法生成代理对象,采用责任链模式,从第一个对象(四大对象之一)开始,将对象传递给plugin方法,返回一个代理,若存在第二个插件,在将第一个代理对象,传递给plugin方法,返回第一个代理对象的代理,以此类推,有多少个拦截器就生成多少个代理对象;
MyBatis提供了一个常用工具类用来生成代理对象,Plugin类:
public class Plugin implements InvocationHandler {
......
//生成动态代理对象
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),interfaces, new Plugin(target, interceptor, signatureMap));
}
return target;
}
//代理对象在调用方法时会进入invoke方法,存在签名的拦截方法,插件的intercept方法就会在这里调用,然后返回结果;否则直接反射调度要执行的方法;
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
//Invocation对象中的proceed方法也是反射调度
return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
......
}
2)MetaObject
MyBatis的工具类,可以有效读取或者修改一些重要对象的属性;因为四大对象本身提供的public设置参数的方法很少;
包含3个方法:
//包装对象,现已被MyBatis的SystemMetaObject.forObject(Object obj)取代
public static MetaObject forObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) {...}
//获取对象属性值,支持OGNL
public Object getValue(String name) {}
//设置对象属性值,支持OGNL
public void setValue(String name, Object value) {}
使用:
StatementHandler handler=(StatementHandler)invocation.getTarget();
//绑定为一个MetaObject对象
MetaObject metaStatementHandler=SystemMetaObject.forObject(handler);
//分离代理对象链,目标类可能存在多个插件拦截,因此需要循环获取最原始的目标类
while(metaStatementHandler.hasGetter("h")){
Object object=metaStatementHandler.getValue("h");
metaStatementHandler=SystemMetaObject.orObject(object);
}
//获取当前调用的SQL,拦截的StatementHandler 实际是RoutingStatementHandler对象,它的delegate属性
//才是真是无的StatementHandler,delegate有一个属性boundSql,boundSql下有sql
String sql=(String)metaStatementHandler.getValue("delegate.boundSql.sql");
......
3)实例:限制每条SQL返回的数据行数,这个行数需要是可配置参数
-确定拦截对象及方法
这里涉及到SQL的执行,因此拦截的是StatementHandler对象,在预编译SQL之前修改SQL,使返回结果数量被修改;方法是StatementHandler的prepare();
-定义拦截器
//@Interceptors表示拦截器,@Signature是注册拦截器签名,只有满足签名条件才能执行
//type拦截对象,四大对象之一,method拦截方法,args方法参数,根据拦截对象的方法参数进行设置
@Interceptors({@Signature(type=StatementHandler.class,method="prepare",
args={Connection.class,Integer})})
public class MyPlugin implements Interceptor{}
-配置拦截器:在MyBatis配置文件
<plugins>
<plugin interceptor="cn.infocore.plugin.MyPlugin">
<property name="dbType" value="mysql"/>
</plugin>
</plugins>
4)分页插件
前文有提到过MyBatis中的一个分页类RowBounds,是基于第一次查询结果再分页,会查询所有记录,性能不高;