前言
在web開發過程中涉及到表格時,例如dataTable,就會產生分頁的需求,通常我們將分頁方式分爲兩種:前端分頁和後端分頁。
前端分頁
一次性請求數據表格中的所有記錄(ajax),然後在前端緩存並且計算count和分頁邏輯,一般前端組件(例如dataTable)會提供分頁動作。
特點是:簡單,很適合小規模的web平臺;當數據量大的時候會產生性能問題,在查詢和網絡傳輸的時間會很長。
後端分頁
在ajax請求中指定頁碼(pageNum)和每頁的大小(pageSize),後端查詢出當頁的數據返回,前端只負責渲染。
特點是:複雜一些;性能瓶頸在MySQL的查詢性能,這個當然可以調優解決。一般來說,web開發使用的是這種方式。
我們說的也是後端分頁。
MySQL對分頁的支持
簡單來說MySQL對分頁的支持是通過limit子句。請看下面的例子。
複製代碼
limit關鍵字的用法是
LIMIT [offset,] rows
offset是相對於首行的偏移量(首行是0),rows是返回條數。
每頁10條記錄,取第一頁,返回的是前10條記錄
select * from tableA limit 0,10;
每頁10條記錄,取第二頁,返回的是第11條記錄,到第20條記錄,
select * from tableA limit 10,10;
複製代碼
這裏提一嘴的是,MySQL在處理分頁的時候是這樣的:
limit 1000,10 - 過濾出1010條數據,然後丟棄前1000條,保留10條。當偏移量大的時候,性能會有所下降。
limit 100000,10 - 會過濾10w+10條數據,然後丟棄前10w條。如果在分頁中發現了性能問題,可以根據這個思路調優。
Mybatis分頁插件PageHelper
在使用Java Spring開發的時候,Mybatis算是對數據庫操作的利器了。不過在處理分頁的時候,Mybatis並沒有什麼特別的方法,一般需要自己去寫limit子句實現,成本較高。好在有個PageHelper插件。
1、POM依賴
Mybatis的配置就不多提了。PageHelper的依賴如下。需要新的版本可以去maven上自行選擇
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>4.1.4</version>
</dependency>
2、Mybatis對PageHelper的配置
打開Mybatis配置文件,一般在Resource路徑下。我這裏叫mybatis-config.xml。
<?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>
<!-- 全局參數 -->
<settings>
<!-- 使全局的映射器啓用或禁用緩存。 -->
<setting name="cacheEnabled" value="true"/>
<!-- 全局啓用或禁用延遲加載。當禁用時,所有關聯對象都會即時加載。 -->
<setting name="lazyLoadingEnabled" value="true"/>
<!-- 當啓用時,有延遲加載屬性的對象在被調用時將會完全加載任意屬性。否則,每種屬性將會按需要加載。 -->
<setting name="aggressiveLazyLoading" value="true"/>
<!-- 是否允許單條sql 返回多個數據集 (取決於驅動的兼容性) default:true -->
<setting name="multipleResultSetsEnabled" value="true"/>
<!-- 是否可以使用列的別名 (取決於驅動的兼容性) default:true -->
<setting name="useColumnLabel" value="true"/>
<!-- 允許JDBC 生成主鍵。需要驅動器支持。如果設爲了true,這個設置將強制使用被生成的主鍵,有一些驅動器不兼容不過仍然可以執行。 default:false -->
<setting name="useGeneratedKeys" value="true"/>
<!-- 指定 MyBatis 如何自動映射 數據基表的列 NONE:不隱射 PARTIAL:部分 FULL:全部 -->
<setting name="autoMappingBehavior" value="PARTIAL"/>
<!-- 這是默認的執行類型 (SIMPLE: 簡單; REUSE: 執行器可能重複使用prepared statements語句;BATCH: 執行器可以重複執行語句和批量更新) -->
<setting name="defaultExecutorType" value="SIMPLE"/>
<!-- 使用駝峯命名法轉換字段。 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
<!-- 設置本地緩存範圍 session:就會有數據的共享 statement:語句範圍 (這樣就不會有數據的共享 ) defalut:session -->
<setting name="localCacheScope" value="SESSION"/>
<!-- 設置但JDBC類型爲空時,某些驅動程序 要指定值,default:OTHER,插入空值時不需要指定類型 -->
<setting name="jdbcTypeForNull" value="NULL"/>
</settings>
<plugins>
<plugin interceptor="com.github.pagehelper.PageHelper">
<property name="dialect" value="mysql"/>
<property name="offsetAsPageNum" value="false"/>
<property name="rowBoundsWithCount" value="false"/>
<property name="pageSizeZero" value="true"/>
<property name="reasonable" value="false"/>
<property name="supportMethodsArguments" value="false"/>
<property name="returnPageInfo" value="none"/>
</plugin>
</plugins>
</configuration>
這裏要注意的是PageHelper相關的配置。
如果你沒有加載Mybatis配置文件,那麼使用的是Mybatis默認的配置。如何加載Mybatis配置文件呢?
到你的dataSrouce配置中。
在配置sqlSessionFactory的時候,指定Mybatis核心配置文件和mapper的路徑,代碼如下
@Bean(name = "moonlightSqlSessionFactory")
@Primary
public SqlSessionFactory moonlightSqlSessionFactory(@Qualifier("moonlightData") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis-mapper/*.xml"));
bean.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
return bean.getObject();
}
說明:
這裏配置的mapper.xml存放路徑,在Resource/mybatis-mapper文件夾下
這裏配置的mybatis-config.xml文件,在Resource/下
3、分頁
準備一個mapper.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.kangaroo.studio.moonlight.dao.mapper.MoonlightMapper">
<resultMap id="geoFenceList" type="com.kangaroo.studio.moonlight.dao.model.GeoFence">
<constructor>
<idArg column="id" javaType="java.lang.Integer" jdbcType="INTEGER" />
<arg column="name" javaType="java.lang.String" jdbcType="VARCHAR" />
<arg column="type" javaType="java.lang.Integer" jdbcType="INTEGER" />
<arg column="group" javaType="java.lang.String" jdbcType="VARCHAR" />
<arg column="geo" javaType="java.lang.String" jdbcType="VARCHAR" />
<arg column="createTime" javaType="java.lang.String" jdbcType="VARCHAR" />
<arg column="updateTime" javaType="java.lang.String" jdbcType="VARCHAR" />
</constructor>
</resultMap>
<sql id="base_column">id, name, type, `group`, geo, createTime, updateTime </sql>
<select id="queryGeoFence" parameterType="com.kangaroo.studio.moonlight.dao.model.GeoFenceQueryParam" resultMap="geoFenceList">
select <include refid="base_column"/> from geoFence where 1=1
<if test="type != null">
and type = #{type}
</if>
<if test="name != null">
and name like concat('%', #{name},'%')
</if>
<if test="group != null">
and `group` like concat('%', #{group},'%')
</if>
<if test="startTime != null">
and createTime >= #{startTime}
</if>
<if test="endTime != null">
and createTime <= #{endTime}
</if>
</select>
</mapper>
在Mapper.java接口中編寫對應的方法
1
List queryGeoFence(GeoFenceQueryParam geoFenceQueryParam);
先上分頁代碼,後面再說明
@RequestMapping(value = "/fence/query", method = RequestMethod.POST)
@ResponseBody
public ResponseEntity<Response> queryFence(@RequestBody GeoFenceQueryParam geoFenceQueryParam) {
try {
Map<String, Object> data = new HashMap<>();
Integer pageNum = geoFenceQueryParam.getPageNum()!=null?geoFenceQueryParam.getPageNum():1;
Integer pageSize = geoFenceQueryParam.getPageSize()!=null?geoFenceQueryParam.getPageSize():10;
Page page = PageHelper.startPage(pageNum, pageSize, true);
List<GeoFence> list = moonlightMapper.queryGeoFence(geoFenceQueryParam);
data.put("total", page.getTotal());
data.put("nowPage", pageNum);
data.put("data", list);
return new ResponseEntity<>(
new Response(ResultCode.SUCCESS, "查詢geoFence成功", data),
HttpStatus.OK);
} catch (Exception e) {
logger.error("查詢geoFence失敗", e);
return new ResponseEntity<>(
new Response(ResultCode.EXCEPTION, "查詢geoFence失敗", null),
HttpStatus.INTERNAL_SERVER_ERROR);
}
}
說明:
1、PageHelper的優點是,分頁和Mapper.xml完全解耦。實現方式是以插件的形式,對Mybatis執行的流程進行了強化,添加了總數count和limit查詢。屬於物理分頁。
2、Page page = PageHelper.startPage(pageNum, pageSize, true); - true表示需要統計總數,這樣會多進行一次請求select count(0); 省略掉true參數只返回分頁數據。
1)統計總數,(將SQL語句變爲 select count(0) from xxx,只對簡單SQL語句其效果,複雜SQL語句需要自己寫)
Page<?> page = PageHelper.startPage(1,-1);
long count = page.getTotal();
2)分頁,pageNum - 第N頁, pageSize - 每頁M條數
A、只分頁不統計(每次只執行分頁語句)
PageHelper.startPage([pageNum],[pageSize]);
List<?> pagelist = queryForList( xxx.class, "queryAll" , param);
//pagelist就是分頁之後的結果
B、分頁並統計(每次執行2條語句,一條select count語句,一條分頁語句)適用於查詢分頁時數據發生變動,需要將實時的變動信息反映到分頁結果上
Page<?> page = PageHelper.startPage([pageNum],[pageSize],[iscount]);
List<?> pagelist = queryForList( xxx.class , "queryAll" , param);
long count = page.getTotal();
//也可以 List<?> pagelist = page.getList(); 獲取分頁後的結果集
3)使用PageHelper查全部(不分頁)
PageHelper.startPage(1,0);
List<?> alllist = queryForList( xxx.class , "queryAll" , param);
4)PageHelper的其他API
String orderBy = PageHelper.getOrderBy(); //獲取orderBy語句
Page<?> page = PageHelper.startPage(Object params);
Page<?> page = PageHelper.startPage(int pageNum, int pageSize);
Page<?> page = PageHelper.startPage(int pageNum, int pageSize, boolean isCount);
Page<?> page = PageHelper.startPage(pageNum, pageSize, orderBy);
Page<?> page = PageHelper.startPage(pageNum, pageSize, isCount, isReasonable); //isReasonable分頁合理化,null時用默認配置
Page<?> page = PageHelper.startPage(pageNum, pageSize, isCount, isReasonable, isPageSizeZero); //isPageSizeZero是否支持PageSize爲0,true且pageSize=0時返回全部結果,false時分頁,null時用默認配置
5)、默認值
//RowBounds參數offset作爲PageNum使用 - 默認不使用
private boolean offsetAsPageNum = false;
//RowBounds是否進行count查詢 - 默認不查詢
private boolean rowBoundsWithCount = false;
//當設置爲true的時候,如果pagesize設置爲0(或RowBounds的limit=0),就不執行分頁,返回全部結果
private boolean pageSizeZero = false;
//分頁合理化
private boolean reasonable = false;
//是否支持接口參數來傳遞分頁參數,默認false
private boolean supportMethodsArguments = false;
3、有一個安全性問題,需要注意一下,不然可能導致分頁錯亂。我這裏直接粘貼了這篇博客裏的一段話。
4. 什麼時候會導致不安全的分頁?
PageHelper 方法使用了靜態的 ThreadLocal 參數,分頁參數和線程是綁定的。
只要你可以保證在 PageHelper 方法調用後緊跟 MyBatis 查詢方法,這就是安全的。因爲 PageHelper 在 finally 代碼段中自動清除了 ThreadLocal 存儲的對象。
如果代碼在進入 Executor 前發生異常,就會導致線程不可用,這屬於人爲的 Bug(例如接口方法和 XML 中的不匹配,導致找不到 MappedStatement 時), 這種情況由於線程不可用,也不會導致 ThreadLocal 參數被錯誤的使用。
但是如果你寫出下面這樣的代碼,就是不安全的用法:
PageHelper.startPage(1, 10);
List<Country> list;
if(param1 != null){
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
這種情況下由於 param1 存在 null 的情況,就會導致 PageHelper 生產了一個分頁參數,但是沒有被消費,這個參數就會一直保留在這個線程上。當這個線程再次被使用時,就可能導致不該分頁的方法去消費這個分頁參數,這就產生了莫名其妙的分頁。
上面這個代碼,應該寫成下面這個樣子:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
list = countryMapper.selectIf(param1);
} else {
list = new ArrayList<Country>();
}
這種寫法就能保證安全。
如果你對此不放心,你可以手動清理 ThreadLocal 存儲的分頁參數,可以像下面這樣使用:
List<Country> list;
if(param1 != null){
PageHelper.startPage(1, 10);
try{
list = countryMapper.selectAll();
} finally {
PageHelper.clearPage();
}
} else {
list = new ArrayList<Country>();
}
這麼寫很不好看,而且沒有必要。
官方文檔,給你參考:
https://pagehelper.github.io/docs/howtouse/