寫一個「特殊」的查詢構造器 - (4、條件查詢:複雜條件)

複雜的條件

在 SQL 的條件查詢中,不僅有 where、or where 這些基本的子句,還有 where in、where exists、where between 等複雜一些的子句。並且即便是 where 這種基礎的子句,也有多個條件的多種邏輯組合。這篇咱們就來說一下查詢構造器如何構造這些複雜的查詢語句。php

where 系列

where in 子句

咱們回想一下使用 where in 子句的 SQL 是什麼樣的:laravel

-- 從一個數據範圍獲取
SELECT * FROM test_table WHERE age IN (18, 20, 22, 24);
-- 從一個子查詢獲取
SELECT * FROM test_table WHERE username IN (SELECT username FROM test_name_table);

從一個子查詢獲取的模式有些複雜咱們稍後再說,先分析下從數據範圍獲取的方式。git

where in 子句判斷字段是否屬於一個數據集合,有 where in、where not in、or where in、or where not in 四種模式。咱們只需構造好這個數據集合,並對集合中的數據進行數據綁定便可。github

基類中添加 whereIn() 方法:sql

// $field where in 要查的字段
// $data  進行判斷的數據集合
// $condition in、not in 模式
// $operator AND、OR 分隔符
public function whereIn($field, array $data, $condition = 'IN', $operator = 'AND')
{
    // 判斷模式和分隔符是否合法
    if( ! in_array($condition, ['IN', 'NOT IN']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Error whereIn mode");
    }
    // 生成佔位符,綁定數據
    foreach ($data as $key => $value) {
        $plh = self::_getPlh();
        $data[$key] = $plh;
        $this->_bind_params[$plh] = $value;
    }
    // 第一次調用該方法,須要 WHERE 關鍵字
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.self::_wrapRow($field).' '.$condition.' ('.implode(',', $data).')';
    } else { // 非初次調用,使用分隔符鏈接
        $this->_where_str .= ' '.$operator.' '.self::_wrapRow($field).' '.$condition.' ('.implode(',', $data).')';
    }
    // 方便鏈式調用,返回當前實例
    return $this;
}

關於上述代碼,因爲 where in、where not in、or where in、or where not in 這寫方法的區別只是關鍵字的區別,對於字符串來講只需替換關鍵字便可。因此對於這些方法,爲了方便,咱們把這些模式的關鍵字做爲方法的參數傳入,能夠提升代碼的重用性。數組

那麼,另外三種模式的代碼能夠這麼寫:閉包

public function orWhereIn($field, array $data)
{
    return $this->whereIn($field, $data, 'IN', 'OR');
}

public function whereNotIn($field, array $data)
{
    return $this->whereIn($field, $data, 'NOT IN', 'AND');
}

public function orWhereNotIn($field, array $data)
{
    return $this->whereIn($field, $data, 'NOT IN', 'OR');
}

構造測試函數

$driver->table('test_table')
       ->whereIn('age', [18, 20, 22, 24])
       ->get();

$driver->table('test_table')
       ->Where('age', '!=', 12)
       ->orWhereNotIn('age', [13, 23, 26, 25])
       ->get();

where between 子句

where between 子句的構造和 where in 相差無幾,只有語法上的區別,並且只有 where between and、or where between and 兩種模式。測試

whereBetween 系列方法代碼:優化

public function whereBetween($field, $start, $end, $operator = 'AND')
{
    // 檢測模式是否合法
    if( ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Logical operator");
    }
    // 生成佔位符,綁定數據
    $start_plh = self::_getPlh();
    $end_plh = self::_getPlh();
    $this->_bind_params[$start_plh] = $start;
    $this->_bind_params[$end_plh] = $end;

    // 是否初次訪問?
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.self::_wrapRow($field).' BETWEEN '.$start_plh.' AND '.$end_plh;
    } else {
        $this->_where_str .= ' '.$operator.' '.self::_wrapRow($field).' BETWEEN '.$start_plh.' AND '.$end_plh;
    }

    return $this;
}

public function orWhereBetween($field, $start, $end)
{
    return $this->whereBetween($field, $start, $end, 'OR');
}

where null 子句

前面的 where 子句中使用單條件模式數據爲 NULL 時則進行 IS NULL 的判斷。可是咱們想要一個更靈活、語義更清晰的接口,因此這裏爲 NULL 的判斷單獨編寫方法。

where null 系列代碼:

public function whereNull($field, $condition = 'NULL', $operator = 'AND')
{
    if( ! in_array($condition, ['NULL', 'NOT NULL']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Logical operator");
    }
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE ';
    } else {
        $this->_where_str .= ' '.$operator.' ';
    }

    $this->_where_str .= self::_wrapRow($field).' IS '.$condition.' ';

    return $this;
}

public function whereNotNull($field)
{
    return $this->whereNull($field, 'NOT NULL', 'AND');
}

public function orWhereNull($field)
{
    return $this->whereNull($field, 'NULL', 'OR');
}

public function orWhereNotNull($field)
{
    return $this->whereNull($field, 'NOT NULL', 'OR');
}

where exists

到 where exists 子句時,構造就有些難度了。咱們回憶一下使用 where exists 子句的 SQL:

SELECT * FROM table1 where exists (SELECT * FROM table2);

沒錯,和以前構造的語句不一樣,where exists 子句存在子查詢。以前的 sql 構造都是經過 _buildQuery() 方法按照必定的順序構造的,那麼如何對子查詢進行構造呢?子查詢中的 where 子句和外層查詢的 where 子句同時存在時,又該怎麼區分呢?

首先,觀察一下有子查詢的 SQL,能夠看出:子查詢是一個獨立的查詢語句。

那麼,能不能將子查詢語句和外層查詢語句各自單獨構造,而後再組合到一塊兒成爲一條完整的 SQL 呢?

固然是能夠的。不過,如何去單獨構造子查詢語句呢?若是子查詢中還有子查詢語句呢?

咱們先看下 laravel 中的 where exists 構造語句是什麼樣的【1】:

DB::table('users')
            ->whereExists(function ($query) {
                $query->select(DB::raw(1))
                      ->from('orders')
                      ->whereRaw('orders.user_id = users.id');
            })
            ->get();

laravel 查詢構造器的 whereExists() 方法接受一個閉包,閉包接收一個查詢構造器實例,用於在閉包中構造子句。

使用閉包的好處是:

  • 給接受閉包參數的函數擴展功能 (進行子查詢語句構造)
  • 閉包傳入函數中,函數能夠控制這個閉包的執行方式,在閉包的執行先後能夠作相應操做 (現場保護、恢復)

基本結構

因此參考 laravel,咱們也使用傳入閉包的方式,咱們先肯定一下 whereExists() 方法的基本結構:

// $callback 閉包參數
// $condition exists、not exists 模式
// $operator and、or 模式
public function whereExists(Closure $callback, $condition = 'EXISTS', $operator = 'AND')
{
    // 判斷模式是否合法
    if( ! in_array($condition, ['EXISTS', 'NOT EXISTS']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Error whereExists mode");
    }
    // 初次調用?
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.$condition.' ( ';
    } else {
        $this->_where_str .= ' '.$operator.' '.$condition.' ( ';
    }

    // 進行現場保護
    ...
    // 閉包調用,傳入當前實例
    ...
    // 現場恢復
    ...

    // 返回當前實例
    return $this;
}

由於使用到了 Closure 限制參數類型,要在基類文件的頂部加上:

use Closure;

現場的保護和恢復

上面一直再說現場的保護和恢復,那麼咱們保護、恢復的這個現場是什麼呢?

咱們先理一下構造一個普通的 SQL 的步驟:依次構造各個查詢子句、使用 _buildQuery() 方法將這些子句按照固定順序組合成 SQL。

那麼在有子查詢的過程當中,意味着這樣的步驟要通過兩次,可是因爲要傳入當前實例 (另外新建實例的話會建立新鏈接),第二次查詢構造會覆蓋掉第一次構造的結果。因此,咱們這裏的現場就是這些構造用的子句字符串。

有了現場的保護和恢復,即便在閉包中調用閉包 (即子查詢中嵌套子查詢) 的情形下也能正確的構造須要的 SQL 語句。(有沒有以爲很像遞歸呢?的確這裏是借鑑了棧的使用思路。)

首先咱們須要一個保存構造字符串名稱的數組 (用來獲取構造字符串屬性),在基類添加屬性 _buildAttrs:

// 這裏保存了須要保護現場的構造字符串名稱
protected $_buildAttrs = [
    '_table',
    '_prepare_sql',
    '_cols_str',
    '_where_str',
    '_orderby_str',
    '_groupby_str',
    '_having_str',
    '_join_str',
    '_limit_str',
];

而後,添加保護現場和恢復現場的方法:

// 保護現場
protected function _storeBuildAttr()
{
    $store = [];
    // 將實例的相關屬性保存到 $store,並返回
    foreach ($this->_buildAttrs as $buildAttr) {
        $store[$buildAttr] = $this->$buildAttr;
    }

    return $store;
}
//恢復現場
protected function _reStoreBuildAttr(array $data)
{
    // 從 $data 取數據恢復當前實例的屬性
    foreach ($this->_buildAttrs as $buildAttr) {
        $this->$buildAttr = $data[$buildAttr];
    }
}

固然,保護了現場後,子查詢要使用實例的屬性時須要的是一個初始狀態的屬性,因此咱們還須要一個能夠重置這些構造字符串的方法:

protected function _resetBuildAttr()
{
    $this->_table = '';
    $this->_prepare_sql = '';
    $this->_cols_str = ' * ';
    $this->_where_str = '';
    $this->_orderby_str = '';
    $this->_groupby_str = '';
    $this->_having_str = '';
    $this->_join_str = '';
    $this->_limit_str = '';
}

完成 whereExists()

有了保護、恢復現場的方法,咱們繼續完成 whereExists() 方法:

public function whereExists(Closure $callback, $condition = 'EXISTS', $operator = 'AND')
{
    if( ! in_array($condition, ['EXISTS', 'NOT EXISTS']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Error whereExists mode");
    }

    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.$condition.' ( ';
    } else {
        $this->_where_str .= ' '.$operator.' '.$condition.' ( ';
    }

    // 保護現場,將構造字符串屬性都保存起來
    $store = $this->_storeBuildAttr();

    /**************** 開始子查詢 SQL 的構造 ****************/
        // 復位構造字符串
        $this->_resetBuildAttr();
        // 調用閉包,將當前實例做爲參數傳入
        call_user_func($callback, $this);
        // 子查詢構造字符串數組    
        $sub_attr = [];
        // 構造子查詢 SQL
        $this->_buildQuery();
        // 保存子查詢構造字符串,用於外層調用
        foreach ($this->_buildAttrs as $buildAttr) {
            $sub_attr[$buildAttr] = $this->$buildAttr;
        }
    /**************** 結束子查詢 SQL 的構造 ****************/

    // 恢復現場
    $this->_reStoreBuildAttr($store);

    // 獲取子查詢 SQL 字符串,構造外層 SQL
    $this->_where_str .= $sub_attr['_prepare_sql'].' ) ';
    
    return $this;
}

測試

構造語句 SELECT * FROM student WHERE EXISTS ( SELECT * FROM classes WHERE id = 3);

$results = $driver->table('student')
                  ->whereExists(function($query) {
                      $query->table('classes')
                            ->where('id', 3);
                  })
                  ->get();

你們在測試文件中試試看吧!

whereNotExists()、orWhereExists() 等模式就不單獨演示了。完整代碼請看 WorkerF - PDODriver.php

優化

where exists 子句用到了子查詢,但並不僅有 where exists 使用子查詢。最直接的 SELECT * FROM (SELECT * FROM table); 子查詢語句,where in 子查詢語句也用到子查詢,那麼重複的邏輯要提出來,Don't Repeat Yourself!

基類中新建 _subBuilder() 方法,用來進行現場的保護恢復、子查詢 SQL 的構造:

protected function _subBuilder(Closure $callback)
{
    // 現場保護
    $store = $this->_storeBuildAttr();

    /**************** begin sub query build ****************/

        $this->_resetBuildAttr();

        call_user_func($callback, $this);
        
        $sub_attr = [];

        $this->_buildQuery();

        foreach ($this->_buildAttrs as $buildAttr) {
            $sub_attr[$buildAttr] = $this->$buildAttr;
        }
    /**************** end sub query build ****************/

    // 現場恢復
    $this->_reStoreBuildAttr($store);

    return $sub_attr;
}

修改 whereExists() 方法:

public function whereExists(Closure $callback, $condition = 'EXISTS', $operator = 'AND')
{
    if( ! in_array($condition, ['EXISTS', 'NOT EXISTS']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Error whereExists mode");
    }

    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.$condition.' ( ';
    } else {
        $this->_where_str .= ' '.$operator.' '.$condition.' ( ';
    }

    $sub_attr = $this->_subBuilder($callback);

    $this->_where_str .= $sub_attr['_prepare_sql'].' ) ';

    return $this;
}

where in 子查詢

有了上面 where exists 的基礎,where in 子查詢的一模一樣:

public function whereInSub($field, Closure $callback, $condition = 'IN', $operator = 'AND')
{
    if( ! in_array($condition, ['IN', 'NOT IN']) || ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Error whereIn mode");
    }
    
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE '.self::_wrapRow($field).' '.$condition.' ( ';
    } else {
        $this->_where_str .= ' '.$operator.' '.self::_wrapRow($field).' '.$condition.' ( ';
    }

    $sub_attr = $this->_subBuilder($callback);
    $this->_where_str .= $sub_attr['_prepare_sql'].' ) ';

    return $this;
}

構造 SQL SELECT * FROM student WHERE class_id IN (SELECT id FROM class);

$results = $driver->table('student')
                ->whereInSub('class_id', function($query) {
                    $query->table('class')->select('id');
                })
                ->get();

一樣,where not in、or where in 這些模式就不單獨展現了。

單純的子查詢

單純的 SELECT * FROM (子查詢) 語句的構造就很簡單了:

public function fromSub(Closure $callback)
{
    $sub_attr = $this->_subBuilder($callback);
    $this->_table .= ' ( '.$sub_attr['_prepare_sql'].' ) AS tb_'.uniqid().' ';

    return $this;
}

上述代碼須要注意的地方:

  • FROM 子查詢語句須要給子查詢一個別名作表名,不然是語法錯誤,這裏咱們選擇 uniqid() 函數生成一個隨機的別名。
  • 這裏是用 _table 屬性保存了子查詢字符串,若是同時調用了 table() 方法會有衝突。

構造 SQL SELECT username, age FROM (SELECT * FROM test_table WHERE class_id = 3)

$results = $driver->select('username', 'age')
            ->fromSub(function($query) {
                $query->table('test_table')->where('class_id', 3);
            })
            ->get();

複雜的 where 邏輯

在基本的 where 子句中,有時候會出現複雜的邏輯運算,好比多個條件用 OR 和 AND 來組合:

WHERE a = 1 OR a = 2 AND b = 1;

AND 的優先級是大於 OR 的,若是想要先執行 OR 的條件,須要圓括號進行包裹:

WHERE a = 1 AND (b = 2 OR c = 3);

AND 和 OR 咱們能夠用 where() 和 orWhere() 方法鏈接,可是圓括號的包裹還須要增長方法來實現。

思路

參考含有子查詢的 SQL,咱們能夠把圓括號包裹的內部做爲一個「子查詢」字符串來看待,區別在於,咱們不像是子查詢構造中取整個子查詢的 SQL,而是隻取 where 子句的構造字符串。

Ok,有了思路,那就編碼吧:

public function whereBrackets(Closure $callback, $operator = 'AND')
{
    if( ! in_array($operator, ['AND', 'OR'])) {
        throw new \InvalidArgumentException("Logical operator");
    }
    
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE ( '; // 開頭的括號包裹
    } else {
        $this->_where_str .= ' '.$operator.' ( '; // 開頭的括號包裹
    }
    $sub_attr = $this->_subBuilder($callback);
    // 這裏只取子查詢構造中的 where 子句
    // 因爲子查詢的 where 子句會帶上 WHERE 關鍵字,這裏要去掉
    $this->_where_str .= preg_replace('/WHERE/', '', $sub_attr['_where_str'], 1).' ) '; // 結尾的括號包裹

    return $this;
}

構造 SQL SELECT * FROM test_table WHERE a = 1 AND (b = 2 OR c IS NOT NULL);

$results = $driver->table('test_table')
            ->where('a', 1)
            ->whereBrackets(function($query) {
                $query->where('b', 2)
                      ->orWhereNotNull('c');
            })
            ->get();

orWhereBrackets() 就不單獨演示了。

參考

【1】Laravel - Query Builder

相關文章
相關標籤/搜索