Mybatis流式查詢之ResultHanlde問題
前言
正常訪問數(shù)據(jù)庫的查詢操作,都是根據(jù)查詢sql一次性返回查詢結(jié)果。
但如果遇到目標數(shù)據(jù)量過大、且需要全量查詢、不能分頁、或者內(nèi)存不想被返回的結(jié)果占用過多等需求時(例如導(dǎo)出excel),就可能需要流式查詢。
1.準備工作
1.1.Mybaits的jar包引入
注:idea必須配置build,否則掃描不到src下的xml文件
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>relife</artifactId> <groupId>org.example</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>relife-object</artifactId> <dependencies> <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>compile</scope> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.20</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> </dependencies> <build> <resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build> </project>
1.2.實體類User
public class User { private String name; private int age; private String id; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getId() { return id; } public void setId(String id) { this.id = id; } }
1.3.mybatis-config.xml和userMapper.xml
mybatis-config.xml(resource根目錄)
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <mapper resource="com/relife/mybatis/userMapper.xml"/> </mappers> </configuration>
userMapper.xml
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.relife.object.User"> <select id="selectUser" resultType="com.relife.object.User"> select id,name,age from user </select> </mapper>
2.流式處理
2.1流式逐條處理handle
public class UserResultHandler implements ResultHandler<User> { @Override public void handleResult(ResultContext<? extends User> resultContext) { // 這里獲取流式查詢每次返回的單條結(jié)果 User user = resultContext.getResultObject(); handle(user); } // 串行逐條執(zhí)行handle private void handle(User user) { System.out.println(user.getId()); } }
適用于導(dǎo)出excel等,,上述寫法缺點也很明顯,單線程,串行,所有效率慢,但是類似導(dǎo)出excel,符合要求,也不太方便使用多線程。
2.2.流式批量多線程處理handle
為了解決效率問題,有時候會對結(jié)果有比如發(fā)送請求,更行其他內(nèi)容等需求時
public class UserResultHandler<T> implements ResultHandler<T> { public final Logger logger = Logger.getLogger(this.getClass()); /** * 線程池線程數(shù) */ private int threadPollNum = 100; public UserResultHandler() { } public UserResultHandler(int threadPollNum) { this.threadPollNum = threadPollNum; } // 線程池 public ExecutorService executorService = Executors.newFixedThreadPool(threadPollNum); // 線程執(zhí)行結(jié)果 public List<Future> futureList = new ArrayList<>(); @Override public void handleResult(ResultContext<? extends T> resultContext) { // 這里獲取流式查詢每次返回的單條結(jié)果 T user = resultContext.getResultObject(); while (futureList.size() > 200) { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } futureList = futureList.stream().filter(future -> !future.isDone()).collect(Collectors.toList()); logger.info("條數(shù):" + resultContext.getResultCount() + "->未完成結(jié)果" + futureList.size()); } UserThread ut = new UserThread(user); Future<?> future = executorService.submit(ut); futureList.add(future); } /** * 保證所有線程執(zhí)行完成,并關(guān)閉線程池 */ public void end() { while (futureList.size() != 0) { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } futureList = futureList.stream().filter(future -> !future.isDone()).collect(Collectors.toList()); } executorService.shutdown(); } public static class UserThread<T> implements Runnable { private T obejct; public UserThread(T obejct) { this.obejct = obejct; } @Override public void run() { System.out.println(((User)obejct).getId()); } public T getObejct() { return obejct; } public void setObejct(T obejct) { this.obejct = obejct; } } }
上述寫法,可用于需要線程返回值的,或者明確需要線程執(zhí)行完成,且可以保證不占用過多內(nèi)存。
其中Thread.sleep()方法,可以自定義時間。
3.測試
3.1.測試案例
public static void main(String[] args) throws IOException { SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder(); SqlSessionFactory sqlSessionFactory = builder.build(Resources.getResourceAsStream("mybatis-config.xml")); System.out.println("sqlSessionFactory:" + sqlSessionFactory); SqlSession sqlSession = sqlSessionFactory.openSession(); // 正常查詢 List<User> userList5 = sqlSession.selectList("selectUser"); // 流式查詢 UserResultHandler userResultHandler = new UserResultHandler(100); sqlSession.select("selectUser", userResultHandler); userResultHandler.end(); sqlSession.close(); }
3.1.正常查詢debug調(diào)用
調(diào)用圖
3.2.流式查詢debug調(diào)用
調(diào)用圖
4.問題與部分源碼解析
4.1加入resulthandle為什么會流式查詢?
在DefaultResultSetHandler類中handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List multipleResults, ResultMapping parentMapping) throws SQLException 方法中出現(xiàn)了調(diào)用區(qū)別一個303行,一個206行
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException { try { if (parentMapping != null) { handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping); } else { // 有的話就使用傳入的ResultHandle,沒有就用默認的DefaultResultHandler if (resultHandler == null) { DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory); handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null); // 指定handle時,把結(jié)果集放到multipleResults中 multipleResults.add(defaultResultHandler.getResultList()); } else { // 非指定handle時,multipleResults中不會有結(jié)果 handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); } } } finally { // issue #228 (close resultsets) closeResultSet(rsw.getResultSet()); } }
這個if判斷直接導(dǎo)致了最后實際調(diào)用callResultHandler時的區(qū)別。
private void callResultHandler(ResultHandler<?> resultHandler, DefaultResultContext<Object> resultContext, Object rowValue) { resultContext.nextResultObject(rowValue); // 加入handle的會調(diào)用自己的代碼實現(xiàn),沒有就用默認的DefaultResultHandler里的方法 ((ResultHandler<Object>) resultHandler).handleResult(resultContext); }
4.2.流式查詢?yōu)槭裁礇]有返回值?
這個可以從兩個方向探討
- 一個是比較直接的,即什么時候返回值變成了void,見DefaultSqlSession
- 一個是為什么會這樣,因為自定義實現(xiàn)是,沒有把結(jié)果加入結(jié)果集,見DefaultResultSetHandler
1.DefaultSqlSession
// 正常查詢 @Override public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) { try { MappedStatement ms = configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } } // 流式查詢 @Override public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { try { MappedStatement ms = configuration.getMappedStatement(statement); // 此方法有返回結(jié)果,但是為空,不需要返回(見下邊方法) executor.query(ms, wrapCollection(parameter), rowBounds, handler); } catch (Exception e) { throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); } finally { ErrorContext.instance().reset(); } }
2.DefaultResultSetHandler
@Override public List<Object> handleResultSets(Statement stmt) throws SQLException { ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); // 初始化返回值 final List<Object> multipleResults = new ArrayList<Object>(); int resultSetCount = 0; ResultSetWrapper rsw = getFirstResultSet(stmt); List<ResultMap> resultMaps = mappedStatement.getResultMaps(); int resultMapCount = resultMaps.size(); validateResultMapsCount(rsw, resultMapCount); while (rsw != null && resultMapCount > resultSetCount) { ResultMap resultMap = resultMaps.get(resultSetCount); // 返回值會在調(diào)用方法時,被賦值,從上述4.1可看出,只有不傳值時才會把結(jié)果放入multipleResults handleResultSet(rsw, resultMap, multipleResults, null); rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } String[] resultSets = mappedStatement.getResultSets(); if (resultSets != null) { while (rsw != null && resultSetCount < resultSets.length) { ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); if (parentMapping != null) { String nestedResultMapId = parentMapping.getNestedResultMapId(); ResultMap resultMap = configuration.getResultMap(nestedResultMapId); handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; } } return collapseSingleResultList(multipleResults); } // 只有一條結(jié)果,就直接取出,多個或空,不處理 private List<Object> collapseSingleResultList(List<Object> multipleResults) { return multipleResults.size() == 1 ? (List<Object>) multipleResults.get(0) : multipleResults; }
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
Mybatis 中的一對一,一對多,多對多的配置原則示例代碼
這篇文章主要介紹了 Mybatis 中的一對一,一對多,多對多的配置原則示例代碼,需要的朋友可以參考下2017-03-03Java的this關(guān)鍵字的使用與方法的重載相關(guān)知識
這篇文章主要介紹了Java的this關(guān)鍵字的使用與方法的重載相關(guān)知識,是Java入門學(xué)習(xí)中的基礎(chǔ)知識,需要的朋友可以參考下2015-09-09第一次使用Android Studio時你應(yīng)該知道的一切配置(推薦)
這篇文章主要介紹了第一次使用Android Studio時你應(yīng)該知道的一切配置(推薦) ,需要的朋友可以參考下2017-09-09