天天看點

MyBatis:實作簡單實體分頁(Plugin的使用)

MyBatis中的SqlSession接口中提供的分頁功能的方法

// 擷取sqlSession的步驟略,statement略,mapper中的映射語句為
// select * from users 
List<User> list = sqlSession.selectList(statement, null, new RowBounds(0,3));
           

MyBatis内置的分頁處理器,是通過記憶體進行分頁,結合上面的例子就是MyBatis首先執行select * from users,然後擷取結果集ResultSet,接着通過傳入的RowBounds中的offset和limit屬性來對ResultSet進行加工。如果記錄量大的話,這種效率無疑是相當低的。想證明上面這個結論,可以檢視MyBatis中的DefaultResultSetHandler類。

我們可以使用mybatis 提供的plugin,實作sql執行前的攔截;在執行sql查詢清單前,裝配指定的分頁select * from users limit #{offset},#{limit}

MyBATIS plugin初始化&原理

MyBATIS是在初始化上下文環境的時候就初始化插件的,mybatis 的plugin實質上就是攔截器。

MyBatis Plugin的實作采用了Java的動态代理,應用了責任鍊設計模式。可以在mybatis-config.xml中加入多個plugin,也就是可以加入多個<plugin>節點,多個攔截器采用鍊式執行。

<plugins>
        <plugin interceptor="com.ljheee.page.interceptor.PageInterceptor">
            <!--property指定分頁參數-->
            <property name="page.limit" value="2"/>
        </plugin>
    </plugins>
           

在MyBatis中,隻能攔截四種接口的實作類:

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler

每種類型的攔截方式都是一樣的。是以我們需要确定攔截什麼對象、什麼方法。

這個需要了解sqlSession的執行原理,可以參考文章:

MyBatis原理第四篇——statementHandler對象(sqlSession内部核心實作,插件的基礎)

從文中可以知道執行查詢是使用StatementHandler的prepare預編譯SQL,使用parameterize設定參數,使用query執行查詢。

我們希望的是在預編譯前去修改sql,做出加入limit語句限制sql的傳回。(這裡我用的是Mysql,如果用其他資料庫需要自己編寫你自己的sql),是以我們要攔截prepare方法。

我們需要做的,除了在mybatis-config.xml中加入<plugin>,最重要的是編碼實作org.apache.ibatis.plugin.Interceptor接口,并指定攔截StatementHandler的prepare()方法。

@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
public class PageInterceptor implements Interceptor {
......
}

           

type 告訴要攔截什麼對象,它可以是四大對象的一個。

method 告訴你要攔截什麼方法。

args 告訴方法的參數是什麼。

實作攔截器

在實作前我們需要熟悉一個mybatis中常用的類的使用。它便是:MetaObject。

它的作用是可以幫助我們取到一些屬性和設定屬性(包括私有的)。它有三個方法:

  • MetaObject forObject(Object object,ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)

這個方法我們基本不用了,因為MyBATIS中可以用SystemMetaObject.forObject(Object obj)代替它。

  • Object getValue(String name)
  • void setValue(String name, Object value)

第一個方法是綁定對象,第二個方法是根據路徑擷取值,第三個方法是擷取值。

這些說還是有點抽象,我們舉個例子,比如說現在我有個學生對象(student),它下面有個屬性學生證(selfcard),學生證也有個屬性發證日期(date)。

但是發證日期是一個私有的屬性且沒有提供公共方法通路。我們現在需要通路它,那麼我們就可以使用MetaObject将其綁定:

MetaObject metaStudent = SystemMetaObject.forObject(student);

這樣便可以讀取它的屬性:

Date date =(Date) metaStudent.getValue("selfcard.date");

或者設定它的屬性:

metaStudent.setValue("selfcard.date", new Date());

MetaObject隻是MyBatis一個工具類。實作sql的攔截和修改,其實就是用metaObject.getValue拿到原來的sql,經過修改後再metaObject.setValue設定到Statement中。

攔截器的實作

package com.ljheee.page.interceptor;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;

import java.sql.Connection;
import java.util.Properties;

import static org.apache.ibatis.reflection.SystemMetaObject.*;

@Intercepts(@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
))
public class PageInterceptor implements Interceptor {

    private int limit = 0;


    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = forObject(statementHandler);

        // 分離代理對象鍊(由于目标類可能被多個攔截器攔截,進而形成多次代理,通過循環可以分離出最原始的的目标類)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = forObject(object);
        }

        //BoundSql對象是處理sql語句的。
        String sql = (String)metaStatementHandler.getValue("delegate.boundSql.sql");

        //判斷sql是否select語句,如果不是select語句那麼就出錯了。
        //如果是修改它,是的它最多傳回行,這裡用的是mysql,其他資料庫要改寫成其他
        if (sql != null && sql.toLowerCase().trim().indexOf("select") == 0 && !sql.contains("$_$limit_$table_")) {
            //通過sql重寫來實作,這裡我們起了一個奇怪的别名,避免表名重複.
            sql = "select * from (" + sql + ") $_$limit_$table_ limit " + this.limit;
            metaStatementHandler.setValue("delegate.boundSql.sql", sql); //重寫SQL
        }
        return invocation.proceed();//實際就是調用原來的prepared方法,隻是再次之前我們修改了sql
    }


    /**
     *通過Plugin的wrap(...)方法來實作代理類的生成操作
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {

        if(target instanceof StatementHandler ){
            return Plugin.wrap(target, this);//使用Plugin的wrap方法生成代理對象
        }else {
            return target;
        }
    }

    @Override
    public void setProperties(Properties props) {
        String limitStr = props.get("page.limit").toString();
        this.limit = Integer.parseInt(limitStr);//用傳遞進來的參數初始化
    }

}
           

工程

https://github.com/ljheee/mybatis-pages

PageHelper介紹

PageHelper分頁插件,支援實體分頁。

PageHelper支援多種資料庫,如Oracle、MySQL、SqlServer、PostgreSQL等,目前最新版本是4.1.6。