寫一個「特殊」的查詢構造器 - (5、聚合函數、分組、排序、分頁)

where 相關的子句構造完成後,咱們繼續構造其它子句。這一篇咱們進行聚合函數、分組、排序等子句的構造。php

聚合函數

在 SQL 中,有一些用來統計、彙總的函數,被稱做聚合函數,如 SUM、COUNT、AVG 等。git

使用 select() 方法時,咱們能夠用 select('COUNT(id)') 這種寫法來使用聚合函數,可是這種方式有缺點:github

  • 語義上並不直觀
  • 在防止關鍵字衝突時須要手動添加引號(參見第三篇條件查詢)
  • 拿到聚合數據須要一串繁瑣的方法調用

爲了更方便的得到聚合數據,咱們須要爲其單獨編寫方法。sql

getList() 方法

得到某一列的方法能夠由 PDO::FETCH_COLUMN 來完成。函數

基類添加 getList() 方法:測試

public function getList($field)
{
    $this->_cols_str = ' '.self::_quote($field).' ';
    $this->_buildQuery();
    $this->_execute();
    // 獲取一列數據
    return $this->_pdoSt->fetchAll(PDO::FETCH_COLUMN, 0);
}

count() 方法

基類添加 count() 方法:fetch

public function count($field = '*')
{
    // 判斷是否時 * 
    // 非 * 時給字段添加引號
    if(trim($field) != '*') {
        $field = self::_quote($field);
    }
    // 構造列查詢字符串
    $this->_cols_str = ' COUNT('.$field.') AS count_num ';
    // 取結果
    return $this->row()['count_num'];
}

構造 SQL SELECT COUNT(id) FROM test_table;ui

$results = $driver->table('test_table')
            ->count('id');

因爲聚合函數方法和 get()、row() 方法同樣是取結果的,鏈式調用時切記要放到最後執行。this

其它方法

sum()、avg() 等方法沒有 COUNT('*') 這樣的場景,比 count() 方法的編寫更簡單。code

sum() 方法:

public function sum($field)
{
    $this->_cols_str = ' SUM('.self::_quote($field).') AS sum_num ';

    return $this->row()['sum_num'];
}

其它方法如 avg()、max() 之類的編寫就不一一展現了,代碼請看 WorkerF - 聚合函數

分組:group by 和 having

group by 子句指定了要分組的字段,能夠是一個或多個 (用逗號隔開)。

having 子句和 group by 子句一塊兒使用,做爲分組的篩選條件,能夠爲空,除了關鍵字外,語法和 where 子句基本相同。

基類新增 groupBy() 方法:

public function groupBy($field)
{
    // 是否初次調用 ?
    if($this->_groupby_str == '') {
        $this->_groupby_str = ' GROUP BY '.self::_wrapRow($field);
    } else { // 非初次調用(多個分組字段),使用逗號分隔
        $this->_groupby_str .= ' , '.self::_wrapRow($field);
    }

    return $this;
}

基類新增 having()、orHaving() 方法:

public function having()
{
    $operator = 'AND';

    // 初次調用 ?
    if($this->_having_str == '') {
        $this->_having_str = ' HAVING ';
    } else {
        $this->_having_str .= ' '.$operator.' ';
    }
    // 和 where 子句同樣進行條件構造
    $this->_condition_constructor(func_num_args(), func_get_args(), $this->_having_str);

    return $this;
}

public function orHaving()
{
    $operator = 'OR';

    if($this->_having_str == '') {
        $this->_having_str = ' HAVING ';
    } else {
        $this->_having_str .= ' '.$operator.' ';
    }

    $this->_condition_constructor(func_num_args(), func_get_args(), $this->_having_str);

    return $this;
}

這裏咱們也留一個處理原生字符串的 havingRaw() 方法 (手動填寫數據,不進行數據綁定):

public function havingRaw($string)
{
    $this->_having_str = ' HAVING '.$string.' ';

    return $this;
}

構造 SQL SELECT id, SUM(price) FROM test_table GROUP BY price HAVING SUM(price) > 1000;

$results = $driver->table('test_table')
            ->select('id', 'SUM(price)')
            ->groupBy('price')
            ->having('SUM(price)', '>', 1000)
            ->get();
// 使用 havingRaw()
$results = $driver->table('test_table')
            ->select('id', 'SUM(price)')
            ->groupBy('price')
            ->havingRaw('SUM(price) > 1000')
            ->get();

排序

排序就很簡單了,固定的語法和正序、倒序兩個模式,多個字段排序時使用逗號隔開:

public function orderBy($field, $mode = 'ASC')
{
    $mode = strtoupper($mode);
    if ( ! in_array($mode, ['ASC', 'DESC'])) {
        throw new \InvalidArgumentException("Error order by mode");
    }
    // 初次調用?
    if($this->_orderby_str == '') {
        $this->_orderby_str = ' ORDER BY '.self::_wrapRow($field).' '.$mode;
    } else {
        // 多個排序字段時,逗號隔開
        $this->_orderby_str .= ' , '.self::_wrapRow($field).' '.$mode;
    }

    return $this;
}

構造 SQL SELECT * FROM tes_table ORDER BY price DESC, id ASC;

$results = $driver->table('test_table')
            ->orderBy('price', 'DESC')
            ->orderBy('id', 'ASC')
            ->get();

limit 和 分頁

limit 子句

標準 SQL 中的 limit 子句是 limit、offset 關鍵字一塊兒使用的,Mysql 中有 LIMIT 0, 10 的簡寫形式,可是 PostgreSql 和 Sqlite 並不適用。因此咱們選用 limit、offset 語法:

public function limit($offset, $step)
{
    $this->_limit_str = ' LIMIT '.$step.' OFFSET '.$offset.' ';

    return $this;
}

構造 SQL SELECT * FROM test_table LIMIT 10 OFFSET 1;

$results = $driver->table('test_table')
            ->limit(1, 10)
            ->get();

分頁

在數據請求時會遇到請求資源數據量大的問題,對於這個問題,廣泛的解決方案就是分頁。

有了 limit() 方法,能夠進行分頁功能的實現了。

爲了靈活的訪問分頁,咱們要返回如下的數據:

  • 數據的總數
  • 每頁的數據個數
  • 當前頁
  • 下一頁
  • 上一頁
  • 第一頁
  • 最後一頁
  • 當前頁的數據集合

對於獲取數據的總數,這裏有一些問題。

如何獲取總數?固然是使用上面講到的聚合函數 count() 來處理。可是,使用了 count() 方法後至關於進行了一次 SQL 的構造和執行 (執行後會將構造字符串設置爲初始狀態),那麼還如何進行當頁數據集合的獲取呢?

兩次執行

固然是有解決方案的,就是構造兩次。

咱們的查詢構造器結構每次新建一個實例就會獲得一個 PDO 的鏈接,因此爲了構造兩次而新建實例的話,代價太大,那麼如何在一個實例中實現兩次執行?

回顧上一篇,含有子查詢的 SQL 構造時通過了兩次構造,對構造字符串進行了保護、恢復。那麼換到分頁中,仍是構造兩次 SQL,對構造字符串進行保護、恢復,區別就是:由於要執行兩次,因此要對綁定的數據也進行保護、恢復。

基類中添加綁定數據保存、恢復的方法:

// 保存綁定數據
protected function _storeBindParam()
{
    return $this->_bind_params;
}
// 恢復綁定數據
protected function _reStoreBindParam($bind_params)
{
    $this->_bind_params = $bind_params;
}

基類中添加 paginate() 方法:

public function paginate($step, $page = NULL)
{
    // 保存構造字符串和綁定數據
    $store = $this->_storeBuildAttr();
    $bind_params = $this->_storeBindParam();
    // 獲取數據總數
    $count = $this->count();
    // 恢復構造字符串和綁定數據
    $this->_reStoreBuildAttr($store);
    $this->_reStoreBindParam($bind_params);

    // 建立分頁數據
    $page = $page ? $page : 1; // 第一頁
    $this->limit($step * ($page - 1), $step);

    $rst['total']        = $count;
    $rst['per_page']     = $step;
    $rst['current_page'] = $page;
    $rst['next_page']    = ($page + 1) > ($count / $step) ? NULL : ($page + 1);
    $rst['prev_page']    = ($page - 1) < 1 ? NULL : ($page - 1);
    $rst['first_page']   = 1;
    $rst['last_page']    = $count / $step;
    $rst['data']         = $this->get();
    // 返回結果
    return $rst;
}

測試一下,獲取從第五頁的數據,每頁 10 條數據:

$results = $driver->table('test_table')
            ->paginate(10, 5);

Just do it!

相關文章
相關標籤/搜索