從分頁查詢談使用者體驗與效能表現
●為什麼要做分頁查詢?
大家登陸網站,使用到查詢功能的時候有沒有發現,其實頁面上幾乎都不會給你展示所有內容,而是以分頁的方式進行展示,我們來看看幾個常見的場景:
CSDN部落格——
站長素材——
Printrest——
包括大家常用的淘寶、知乎、微博、視訊網站等,無一例外都是採用了分頁查詢的機制,具體表現在:
1、查詢的資料量相對較多,每次給使用者展示一部分;
2、使用者通過上一頁、下一頁、頁碼跳轉、滾動條(瀑布流網站)等方式獲取其餘的資料。
這麼做的原因有以下幾點:
1、當資料量很大的時候,後臺全部查詢出來是一個很耗時間的操作;
2、退一步說,就算了用到分散式、快取等技術,降低後臺操作時間,但大量資料在網路上傳輸給使用者時也是很耗時的,例如目前常見的網路環境也不過就是10M或者100M;
3、再退一步說,就算查詢和傳輸都是迅速完成的,把這麼多資料,全部展現在使用者面前,讓使用者自己去大海撈針般的檢視,體驗也是很糟糕的。
因此,無論是從效能表現上還是使用者體驗上,分頁查詢是必須要做的。
●如何實現分頁查詢
按筆者的理解,分頁查詢可以分為兩類,一類稱之為“真分頁”;一類稱之為“假分頁”。
“真分頁”是在後臺按需查詢所要顯示的資料,回傳給前臺展示;“假分頁”是後臺查詢出所有資料,回傳給前臺,由前臺來進行分頁展示。毫無疑問,“假分頁”是一種自欺欺人的做法。
“真分頁”根據實現的技術,筆者也將其分為兩類,一類利用同步阻塞,一類利用非同步通知。前者等待資料到達頁面之後使用者才可以進行其他操作,後者這是利用Ajax,不對使用者的操作產生阻塞,表現在使用者可以點選其他按鈕/選單,進行別的操作。正常來說,都是選擇非同步通知的方式進行真分頁。
流程上來說,使用者設定好查詢條件(例如輸入查詢起止時間、查詢的類別等),點選查詢按鈕(當然,不同的網站表現也不同,例如有的是載入網頁後直接查詢出一些分頁內容,例如點選淘寶的已購買的寶貝,就會自動按時間排序查出最新的X條資料,之後使用者可以設定查詢條件再點查詢),頁面向伺服器後臺發起查詢請求,伺服器根據查詢條件,拼接好查詢SQL語句並執行,查出滿足條件的前XX條資料,並且記錄下總的記錄條數,通過分頁物件返回給前臺。前臺翻頁的時候會把當前頁數、每頁展示資料量等資訊告訴伺服器後臺,繼而查詢之後的資料。
整個流程的時序圖如下:
值得注意的是,使用者進行首次查詢的時候,網頁其實是發起了兩次請求,將查詢資料(設定了顯示數量、類別等條件)和查詢總記錄的條數分開請求,如果不分開,count(*)的操作可能會消耗大量時間,造成使用者遲遲無法看到返回的資料。因此,先把資料展示給使用者,改善使用者的體驗。而在之後的翻頁操作中,就不用再去查詢記錄的總條數了,因為第一次已經查詢過,並且給到了前臺計算總頁數並儲存,之後的翻頁操作,前臺除了將查詢的限制條件發給後臺外,再講查詢的起始值也發過去就可以了。例如每頁顯示20條資料,使用者翻頁到第五頁,那麼起始值就應該是20*(5-1)=80。第一次點選查詢的時候頁面傳過去的起始值是0。
●分頁物件的設計
通常分頁物件需要設計包括以下成員變數:頁面大小(即一頁展示多少條資料)、資料起始id、總的記錄條數、最後一條資料的id、資料(一般是一個List物件)、
通常分頁物件還需要設計包括以下方法:相應的get/set方法、取總頁數的方法(當然也可以把這個邏輯下放到前臺去執行)、取當前頁碼的方法(同前)、是否有上下頁以及一些對應的資料所在位置的計算函式。
來看一下具體的程式碼實現:
import java.util.ArrayList;
public class Page {
// 常量,定義預設的頁面大小,即一頁預設展示多少資料
private static int DEFAULT_PAGE_SIZE = 10;
// 每頁的記錄數,先設為預設值
private int pageSize = DEFAULT_PAGE_SIZE;
// 當前頁第一條資料在List中的位置,從0開始
private long start;
// 當前頁中存放的記錄,型別一般為List<T>
private Object data;
// 總記錄數
private long totalCount;
// 最大的一條記錄的id
private String recordMaxIds;
/**
* 構造方法,構造空頁.
*/
public Page() {
this(0, 0, DEFAULT_PAGE_SIZE, new ArrayList());
}
/**
* 預設構造方法.
* @param start 本頁資料在資料庫中的起始位置
* @param totalSize 資料庫中總記錄條數
* @param pageSize 本頁容量
* @param data 本頁裡面的資料
*/
public Page(long start, long totalSize, int pageSize, Object data) {
if(pageSize == 0 ){
pageSize = DEFAULT_PAGE_SIZE;
}else{
this.pageSize = pageSize;
}
this.start = start;
this.totalCount = totalSize;
this.data = data;
}
/**
* 取總記錄數.
*/
public long getTotalCount() {
return this.totalCount;
}
/**
* 取總頁數.
*/
public long getTotalPageCount() {
if(totalCount == 0){
return 1;
}else{
if (totalCount % pageSize == 0)
return totalCount / pageSize;
else
return totalCount / pageSize + 1;
}
}
/**
* 取每頁資料容量.
*/
public int getPageSize() {
return pageSize;
}
/**
* 取當前頁中的記錄.
*/
public Object getResult() {
return data;
}
/**
* 取該頁當前頁碼,頁碼從1開始.
*/
public long getCurrentPageNo() {
if(pageSize != 0){
return start / pageSize + 1;
}else{
return 1;
}
}
/**
* 該頁是否有下一頁.
*/
public boolean hasNextPage() {
return this.getCurrentPageNo() < this.getTotalPageCount() - 1;
}
/**
* 該頁是否有上一頁.
*/
public boolean hasPreviousPage() {
return this.getCurrentPageNo() > 1;
}
/**
* 獲取任一頁第一條資料在資料集的位置.
* @param pageNo 從1開始的頁號
* @param pageSize 每頁記錄條數
* @return 該頁第一條資料
*/
public static int getStartOfPage(int pageNo, int pageSize) {
return (pageNo - 1) * pageSize;
}
/**
* 獲取頁號,從1開始.
* @param startIndex 開始索引
* @param pageSize 每頁記錄條數
* @return 從1開始的頁號
*/
public static int getPageNo(int startIndex, int pageSize) {
return startIndex % pageSize == 0 ? startIndex / pageSize : startIndex / pageSize + 1;
}
public String getRecordMaxIds() {
return recordMaxIds;
}
public void setRecordMaxIds(String recordMaxIds) {
this.recordMaxIds = recordMaxIds;
}
public void setData(Object data) {
this.data = data;
}
}
●其他補充的資訊
最後,我們要知道,分頁查詢最終還是要落實到資料庫去執行,通過分頁查詢的SQL是進行查詢時避免全表掃描。因此,分頁這個業務,在執行資料庫操作時,需要構造不同的查詢語句,通常來說MySql利用的是limit子句,PostgreSQL利用的是limit和offset子句來實現的,可以人為去Dao層寫對應的函式。如果使用了Hibernate等框架,還可以直接使用它所提供的函式去進行資料庫分頁,例如Hibernate中的setFirstResult()和setMaxResults()函式來控制查詢與返回結果集的條數。而這個控制數量、偏移量的值則是第一次查詢出總記錄,加上頁面設定的每頁資料量多少來共同計算的。
分頁物件回傳給前端網頁的時候,一般可以採用JQuery的Ajax技術進行接收處理,筆者目前從事後端開發工作,這一塊暫時就先不和大家分享了。今天,你學會了嗎?