天天看點

Mybatis插件原理分析(三)分頁插件

在Mybatis中插件最經常使用的是作為分頁插件,接下來我們通過實作Interceptor來完成一個分頁插件。

雖然Mybatis也提供了分頁操作,通過在sqlSession的接口函數中設定RowBounds,給RowBounds設定初值(RowBounds源碼)來實作邏輯分頁,其實作原理就是通過sql查詢所有的結果,并将結果放到List中,然後根據RowBouds的limit和offset數值來傳回最後的資料,這種邏輯分頁在資料量比較大的情況下對性能是有影響的。雖然我們可以自己寫帶有分頁語句的sql來實作實體分頁,如mysql下:select * from table limit 10 offset 1,來獲得分頁結果,但不同的資料庫産品(mysql、oracle、sqlserver和oracle)對應的分頁語句并不相同,如果要同時相容所有的資料庫産品,需要開發人員在每個分頁sql中都編碼幾種資料庫産品的分頁語句,這樣的開發效率實在是太低了,Mybatis的插件Interceptor給了我們一種根據資料庫類型自動組裝sql語句的方法。

PageInterceptor源碼如下:

package com.tianjunwei.page;

import java.lang.reflect.Constructor;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.mapping.MappedStatement.Builder;

import com.tianjunwei.page.dialect.Dialect;
import com.tianjunwei.page.dialect.DialectFactory;

/*
 * 分頁插件我們隻需要攔截Executor的query方法即可,在執行sql語句之前組裝新的分頁sql語句
 */
@Intercepts({@Signature(
    type= Executor.class,
    method = "query",
    args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
  public class PageInterceptor implements Interceptor{
      String dialectClass;
      boolean asyncTotalCount = false;
      String dataBaseType=null;
      public static ThreadLocal<RowBounds> PageRowBounds = new ThreadLocal<RowBounds>();

    @SuppressWarnings({"rawtypes", "unchecked"})
    public Object intercept(final Invocation invocation) throws Throwable {
      //Executor的實作類
          final Executor executor = (Executor) invocation.getTarget();
          //Executor的query函數的參數
          final Object[] queryArgs = invocation.getArgs();
          final MappedStatement ms = (MappedStatement)queryArgs[0];
          final Object parameter = queryArgs[1];
          //rowBounds中有分頁語句的limit和offset值
          RowBounds rowBounds = (RowBounds)queryArgs[2];
          
          if((PageRowBounds.get() != null)&&(PageRowBounds.get().getLimit() != RowBounds.NO_ROW_LIMIT || PageRowBounds.get().getOffset() != RowBounds.NO_ROW_OFFSET)){
            rowBounds = PageRowBounds.get();
          }
          
          //如果不需要分頁操作,直接傳回,rowBounds為預設值時
          if(rowBounds.getOffset() == RowBounds.NO_ROW_OFFSET
                  && rowBounds.getLimit() == RowBounds.NO_ROW_LIMIT){
              return invocation.proceed();
          }
          
          //根據不同的資料庫擷取不到的分頁方言來
          if(dialectClass == null || "".equals(dialectClass)){
          
            //判斷資料源選擇方言,暫時支援mysql、oracle、postgresql和sql server 2005 2008及2012
            Connection connection = executor.getTransaction().getConnection();
            DatabaseMetaData databaseMetaData = null;
            if(connection != null){
              databaseMetaData = connection.getMetaData();
            }else {
          throw new Exception("connection is null");
        }
             
            String databaseProductName = databaseMetaData.getDatabaseProductName();
            if( dataBaseType == null || "".equals(dataBaseType)){
              dataBaseType = databaseProductName;
            }
            //通過xml方言的配置來獲得方言類
            if(databaseProductName != null && !("".equals(dataBaseType))){
              
              dialectClass = DialectFactory.getDialectClass(dataBaseType,databaseProductName);
            
            }else{
              throw new Exception("the property of dialect is null");
            }
            setDialectClass(dialectClass);
          }
          final Dialect dialect;
          try {
            //初始化分頁方言類
              Class clazz = Class.forName(dialectClass);
              Constructor constructor = clazz.getConstructor(new Class[]{MappedStatement.class, Object.class, RowBounds.class});
              dialect = (Dialect)constructor.newInstance(new Object[]{ms, parameter, rowBounds});
          
          } catch (Exception e) {
              throw new ClassNotFoundException("Cannot create dialect instance: "+dialectClass,e);
          }
          final BoundSql boundSql = ms.getBoundSql(parameter);
          //建立新的MappedStatement,此時的sql語句已經是符合資料庫産品的分頁語句
          //dialect.getPageSQL()獲得分頁語句
          //dialect.getParameterMappings(), dialect.getParameterObject(),添加了兩個參數及其值,兩個參數為_limit和_offset
          queryArgs[0] = copyFromNewSql(ms,boundSql,dialect.getPageSQL(), dialect.getParameterMappings(), dialect.getParameterObject());
          //sql語句的參數集合
          queryArgs[1] = dialect.getParameterObject();
          //設定為不分頁,由新的sql語句進行實體分頁
          queryArgs[2] = new RowBounds(RowBounds.NO_ROW_OFFSET,RowBounds.NO_ROW_LIMIT);
          return invocation.proceed();
    }
    
    //這個方法是用于mybatis接口程式設計過程中顯示的指定分頁參數
    public static void setPage(int pageNumber,int pageSize){
      RowBounds pageRowBounds = null;
      if(pageNumber > 0)
        pageRowBounds = new RowBounds((pageNumber-1)*pageSize, pageSize);
      else {
        pageRowBounds = new RowBounds(0, pageSize);
      }
      PageRowBounds.set(pageRowBounds);
    }
    
    //建立新的MappedStatement
      private MappedStatement copyFromNewSql(MappedStatement ms, BoundSql boundSql,
                                             String sql, List<ParameterMapping> parameterMappings, Object parameter){
        //根據新的分頁sql語句建立BoundSql
          BoundSql newBoundSql = copyFromBoundSql(ms, boundSql, sql, parameterMappings, parameter);
          //根據newBoundSql建立新的MappedStatement
          return copyFromMappedStatement(ms, new BoundSqlSqlSource(newBoundSql));
      }
      
      //根據新的分頁sql語句建立BoundSql
    private BoundSql copyFromBoundSql(MappedStatement ms, BoundSql boundSql,
        String sql, List<ParameterMapping> parameterMappings,Object parameter) {
      BoundSql newBoundSql = new BoundSql(ms.getConfiguration(),sql, parameterMappings, parameter);
      for (ParameterMapping mapping : boundSql.getParameterMappings()) {
          String prop = mapping.getProperty();
          if (boundSql.hasAdditionalParameter(prop)) {
              newBoundSql.setAdditionalParameter(prop, boundSql.getAdditionalParameter(prop));
          }
      }
      return newBoundSql;
    }

    //根據newBoundSql建立新的MappedStatement
    private MappedStatement copyFromMappedStatement(MappedStatement ms,SqlSource newSqlSource) {
      Builder builder = new Builder(ms.getConfiguration(),ms.getId(),newSqlSource,ms.getSqlCommandType());
      
      builder.resource(ms.getResource());
      builder.fetchSize(ms.getFetchSize());
      builder.statementType(ms.getStatementType());
      builder.keyGenerator(ms.getKeyGenerator());
      if(ms.getKeyProperties() != null && ms.getKeyProperties().length !=0){
              StringBuffer keyProperties = new StringBuffer();
              for(String keyProperty : ms.getKeyProperties()){
                  keyProperties.append(keyProperty).append(",");
              }
              keyProperties.delete(keyProperties.length()-1, keyProperties.length());
        builder.keyProperty(keyProperties.toString());
      }
      //setStatementTimeout()
      builder.timeout(ms.getTimeout());
      //setStatementResultMap()
      builder.parameterMap(ms.getParameterMap());
      //setStatementResultMap()
          builder.resultMaps(ms.getResultMaps());
      builder.resultSetType(ms.getResultSetType());
      //setStatementCache()
      builder.cache(ms.getCache());
      builder.flushCacheRequired(ms.isFlushCacheRequired());
      builder.useCache(ms.isUseCache());
      return builder.build();
    }

    public Object plugin(Object target) {
      return Plugin.wrap(target, this);
    }

    /**
     * @Title: setProperties 
     * @Description: 方言插件配置時設定的參數
     * @param properties 參數
     * @return  void  
     * @2016年1月13日下午3:54:47
     */
    public void setProperties(Properties properties) {
      dataBaseType = properties.getProperty("dialectType");
    }
    
    public static class BoundSqlSqlSource implements SqlSource {
      BoundSql boundSql;
      public BoundSqlSqlSource(BoundSql boundSql) {
        this.boundSql = boundSql;
      }
      public BoundSql getBoundSql(Object parameterObject) {
        return boundSql;
      }
    }

      public void setDialectClass(String dialectClass) {
          this.dialectClass = dialectClass;
      }

      public void setDataBaseType(String dataBaseType) {
      this.dataBaseType = dataBaseType;
    }
      

}