MyBatis简介(五)

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,是基于第一次查询结果再分页,会查询所有记录,性能不高;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值