小夥伴們元宵節快樂,記得吃元宵哦~java
在平常開發中,小夥伴們多多少少都有用過 MyBatis 插件,鬆哥猜想你們用的最多的就是 MyBatis 的分頁插件!不知道小夥伴們有沒有想過有一天本身也來開發一個 MyBatis 插件?sql
其實本身動手擼一個 MyBatis 插件並不難,今天鬆哥就把手帶你們擼一個 MyBatis 插件!數據庫
即便你沒開發過 MyBatis 插件,估計也能猜出來,MyBatis 插件是經過攔截器來起做用的,MyBatis 框架在設計的時候,就已經爲插件的開發預留了相關接口,以下:數組
public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; default Object plugin(Object target) { return Plugin.wrap(target, this); } default void setProperties(Properties properties) { // NOP } }
這個接口中就三個方法,第一個方法必須實現,後面兩個方法都是可選的。三個方法做用分別以下:mybatis
<plugins> <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor"> <property name="xxx" value="xxx"/> </plugin> </plugins>
攔截器定義好了後,攔截誰?app
這個就須要攔截器簽名來完成了!框架
攔截器簽名是一個名爲 @Intercepts 的註解,該註解中能夠經過 @Signature 配置多個簽名。@Signature 註解中則包含三個屬性:ide
一個簡單的簽名可能像下面這樣:學習
@Intercepts(@Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class} )) public class CamelInterceptor implements Interceptor { //... }
根據前面的介紹,被攔截的對象主要有以下四個:測試
Executor
public interface Executor { ResultHandler NO_RESULT_HANDLER = null; int update(MappedStatement ms, Object parameter) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException; <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException; List<BatchResult> flushStatements() throws SQLException; void commit(boolean required) throws SQLException; void rollback(boolean required) throws SQLException; CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql); boolean isCached(MappedStatement ms, CacheKey key); void clearLocalCache(); void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType); Transaction getTransaction(); void close(boolean forceRollback); boolean isClosed(); void setExecutorWrapper(Executor executor); }
各方法含義分別以下:
ParameterHandler
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement ps) throws SQLException; }
各方法含義分別以下:
ResultSetHandler
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; }
各方法含義分別以下:
StatementHandler
public interface StatementHandler { Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException; void parameterize(Statement statement) throws SQLException; void batch(Statement statement) throws SQLException; int update(Statement statement) throws SQLException; <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException; <E> Cursor<E> queryCursor(Statement statement) throws SQLException; BoundSql getBoundSql(); ParameterHandler getParameterHandler(); }
各方法含義分別以下:
defaultExecutorType=」BATCH」
,執行數據操做時該方法會被調用。在開發一個具體的插件時,咱們應當根據本身的需求來決定到底攔截哪一個方法。
MyBatis 中提供了一個不太好用的內存分頁功能,就是一次性把全部數據都查詢出來,而後在內存中進行分頁處理,這種分頁方式效率很低,基本上沒啥用,可是若是咱們想要自定義分頁插件,就須要對這種分頁方式有一個簡單瞭解。
內存分頁的使用方式以下,首先在 Mapper 中添加 RowBounds 參數,以下:
public interface UserMapper { List<User> getAllUsersByPage(RowBounds rowBounds); }
而後在 XML 文件中定義相關 SQL:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User"> select * from user </select>
能夠看到,在 SQL 定義時,壓根不用管分頁的事情,MyBatis 會查詢到全部的數據,而後在內存中進行分頁處理。
Mapper 中方法的調用方式以下:
@Test public void test3() { UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class); RowBounds rowBounds = new RowBounds(1,2); List<User> list = userMapper.getAllUsersByPage(rowBounds); for (User user : list) { System.out.println("user = " + user); } }
構建 RowBounds 時傳入兩個參數,分別是 offset 和 limit,對應分頁 SQL 中的兩個參數。也能夠經過 RowBounds.DEFAULT 的方式構建一個 RowBounds 實例,這種方式構建出來的 RowBounds 實例,offset 爲 0,limit 則爲 Integer.MAX_VALUE,也就至關於不分頁。
這就是 MyBatis 中提供的一個很不實用的內存分頁功能。
瞭解了 MyBatis 自帶的內存分頁以後,接下來咱們就能夠來看看如何自定義分頁插件了。
首先要聲明一下,這裏鬆哥帶你們自定義 MyBatis 分頁插件,主要是想經過這個東西讓小夥伴們瞭解自定義 MyBatis 插件的一些條條框框,瞭解整個自定義插件的流程,分頁插件並非咱們的目的,自定義分頁插件只是爲了讓你們的學習過程變得有趣一些而已。
接下來咱們就來開啓自定義分頁插件之旅。
首先咱們須要自定義一個 RowBounds,由於 MyBatis 原生的 RowBounds 是內存分頁,而且沒有辦法獲取到總記錄數(通常分頁查詢的時候咱們還須要獲取到總記錄數),因此咱們自定義 PageRowBounds,對原生的 RowBounds 功能進行加強,以下:
public class PageRowBounds extends RowBounds { private Long total; public PageRowBounds(int offset, int limit) { super(offset, limit); } public PageRowBounds() { } public Long getTotal() { return total; } public void setTotal(Long total) { this.total = total; } }
能夠看到,咱們自定義的 PageRowBounds 中增長了 total 字段,用來保存查詢的總記錄數。
接下來咱們自定義攔截器 PageInterceptor,以下:
@Intercepts(@Signature( type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} )) public class PageInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); MappedStatement ms = (MappedStatement) args[0]; Object parameterObject = args[1]; RowBounds rowBounds = (RowBounds) args[2]; if (rowBounds != RowBounds.DEFAULT) { Executor executor = (Executor) invocation.getTarget(); BoundSql boundSql = ms.getBoundSql(parameterObject); Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters"); additionalParametersField.setAccessible(true); Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql); if (rowBounds instanceof PageRowBounds) { MappedStatement countMs = newMappedStatement(ms, Long.class); CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql); String countSql = "select count(*) from (" + boundSql.getSql() + ") temp"; BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject); Set<String> keySet = additionalParameters.keySet(); for (String key : keySet) { countBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql); Long count = (Long) countQueryResult.get(0); ((PageRowBounds) rowBounds).setTotal(count); } CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql); pageKey.update("RowBounds"); String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit(); BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject); Set<String> keySet = additionalParameters.keySet(); for (String key : keySet) { pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key)); } List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql); return list; } //不須要分頁,直接返回結果 return invocation.proceed(); } private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) { MappedStatement.Builder builder = new MappedStatement.Builder( ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType() ); ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build(); builder.resource(ms.getResource()) .fetchSize(ms.getFetchSize()) .statementType(ms.getStatementType()) .timeout(ms.getTimeout()) .parameterMap(ms.getParameterMap()) .resultSetType(ms.getResultSetType()) .cache(ms.getCache()) .flushCacheRequired(ms.isFlushCacheRequired()) .useCache(ms.isUseCache()) .resultMaps(Arrays.asList(resultMap)); if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) { StringBuilder keyProperties = new StringBuilder(); for (String keyProperty : ms.getKeyProperties()) { keyProperties.append(keyProperty).append(","); } keyProperties.delete(keyProperties.length() - 1, keyProperties.length()); builder.keyProperty(keyProperties.toString()); } return builder.build(); } }
這是咱們今天定義的核心代碼,涉及到的知識點鬆哥來給你們一個一個剖析。
invocation.getArgs()
獲取攔截方法的參數,獲取到的是一個數組,正常來講這個數組的長度爲 4。數組第一項是一個 MappedStatement,咱們在 Mapper.xml 中定義的各類操做節點和 SQL,都被封裝成一個個的 MappedStatement 對象了;數組第二項就是所攔截方法的具體參數,也就是你在 Mapper 接口中定義的方法參數;數組的第三項是一個 RowBounds 對象,咱們在 Mapper 接口中定義方法時不必定使用了 RowBounds 對象,若是咱們沒有定義 RowBounds 對象,系統會給咱們提供一個默認的 RowBounds.DEFAULT;數組第四項則是一個處理返回值的 ResultHandler。return invocation.proceed();
,讓方法繼續往下走就好了。在前面的代碼中,咱們一共在兩個地方從新組織了 SQL,一個是查詢總記錄數的時候,另外一個則是分頁的時候,都是經過 boundSql.getSql() 獲取到 Mapper.xml 中的 SQL 而後進行改裝,有的小夥伴在 Mapper.xml 中寫 SQL 的時候不注意,結尾可能加上了 ;
,這會致使分頁插件從新組裝的 SQL 運行出錯,這點須要注意。鬆哥在 GitHub 上看到的其餘 MyBatis 分頁插件也是同樣的,Mapper.xml 中 SQL 結尾不能有 ;
。
如此以後,咱們的分頁插件就算是定義成功了。
接下來咱們對咱們的分頁插件進行一個簡單測試。
首先咱們須要在全局配置中配置分頁插件,配置方式以下:
<plugins> <plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin> </plugins>
接下來咱們在 Mapper 中定義查詢接口:
public interface UserMapper { List<User> getAllUsersByPage(RowBounds rowBounds); }
接下來定義 UserMapper.xml,以下:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User"> select * from user </select>
最後咱們進行測試:
@Test public void test3() { UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class); List<User> list = userMapper.getAllUsersByPage(new RowBounds(1,2)); for (User user : list) { System.out.println("user = " + user); } }
這裏在查詢時,咱們使用了 RowBounds 對象,就只會進行分頁,而不會統計總記錄數。須要注意的時,此時的分頁已經不是內存分頁,而是物理分頁了,這點咱們從打印出來的 SQL 中也能看到,以下:
能夠看到,查詢的時候就已經進行了分頁了。
固然,咱們也可使用 PageRowBounds 進行測試,以下:
@Test public void test4() { UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class); PageRowBounds pageRowBounds = new PageRowBounds(1, 2); List<User> list = userMapper.getAllUsersByPage(pageRowBounds); for (User user : list) { System.out.println("user = " + user); } System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal()); }
此時經過 pageRowBounds.getTotal() 方法咱們就能夠獲取到總記錄數。
好啦,今天主要和小夥伴們分享了咱們如何本身開發一個 MyBatis 插件,插件功能其實都是次要的,最主要是但願小夥伴們可以理解 MyBatis 的工做流程。