1. 程式人生 > >從分頁查詢談使用者體驗與效能表現

從分頁查詢談使用者體驗與效能表現

●為什麼要做分頁查詢?

大家登陸網站,使用到查詢功能的時候有沒有發現,其實頁面上幾乎都不會給你展示所有內容,而是以分頁的方式進行展示,我們來看看幾個常見的場景:

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技術進行接收處理,筆者目前從事後端開發工作,這一塊暫時就先不和大家分享了。今天,你學會了嗎?