寫一個“特殊”的查詢構(gòu)造器 - (三畏铆、條件查詢)

構(gòu)造 where 條件

如果單單是執(zhí)行 SELECT * FROM test_table; 這樣的語句雷袋,使用原生擴(kuò)展就好了,使用查詢構(gòu)造器就是殺雞用牛刀辞居。當(dāng)然楷怒,在實際的業(yè)務(wù)需求中,大部分的 SQL 都沒這么簡單瓦灶,有各種條件查詢鸠删、分組、排序贼陶、連表等操作冶共,尤其是條件查詢,占到了查詢業(yè)務(wù)的大多數(shù)每界。

這一篇,我們來講講如何使用查詢構(gòu)造器進(jìn)行條件查詢家卖。

條件查詢的核心:參數(shù)綁定

首先眨层,我們回顧一下用 PDO 來寫條件查詢該怎么做:

1、構(gòu)造語句上荡、預(yù)編譯:

PDO 可以通過占位符綁定參數(shù)趴樱,占位符可以使用 :name 的形式或者 ? 的形式。

$pdoSt = $pdo->prepare("SELECT * FROM test_table WHERE username = :username AND age = :age;");

2酪捡、進(jìn)行參數(shù)綁定叁征,執(zhí)行語句:

PDOStatement::bindParam()PDOStatement::bindValue() 方法可以綁定一個 PHP 變量到指定的占位符。

$username = 'test';
$age = 18;

$pdoSt->bindValue(':username', $username, PDO::PARAM_STR);
$pdoSt->bindValue(':age', $age, PDO::PARAM_INT);
$pdoSt->execute();

由此我們得知逛薇,只要搞定了參數(shù)綁定捺疼,就可以構(gòu)造一個簡單的 where 子句了。

占位符和綁定方法的選擇

占位符選擇:

? 占位符必須按照順序去綁定永罚,而 :name 占位符只要占位符和數(shù)據(jù)的映射關(guān)系確定啤呼,綁定的數(shù)據(jù)就不會出錯卧秘。所以我們選擇 :name 占位符。

綁定方法的選擇:

PDOStatement::bindValue() 方法把一個值綁定到一個參數(shù)官扣。

PDOStatement::bindParam() 不同于 PDOStatement::bindValue()翅敌,綁定變量作為引用被傳入,并只在 PDOStatement::execute() 被調(diào)用的時候才取值惕蹄。

這里我們選擇 PDOStatement::bindValue() 方法蚯涮,因為參數(shù)綁定過程和 execute 執(zhí)行過程可能被封裝到不同的方法中,我們需要簡單的傳值傳遞而不是引用傳遞卖陵。

為基類編寫參數(shù)綁定方法

先回顧下基類現(xiàn)在執(zhí)行 sql 的過程:在 get()遭顶、row() 這些取結(jié)果的方法中,先執(zhí)行構(gòu)造 sql 的方法赶促,再執(zhí)行 _execute() 方法執(zhí)行 sql液肌。那么也就是說,我們只要在這兩個方法中間進(jìn)行參數(shù)的綁定即可鸥滨。

當(dāng)然嗦哆,并非只有 where 子句需要參數(shù)綁定,having 子句婿滓、where in 子句等也涉及到參數(shù)的綁定老速。為了程序結(jié)構(gòu)的靈活和清晰,我們在基類新加一個 _bind_params 屬性凸主,鍵值數(shù)組類型橘券,用來存儲占位符和其綁定數(shù)據(jù)的映射。這樣卿吐,我們只需:

  • 構(gòu)造 where (having 等) 子句字符串時生成占位符旁舰,并將占位符和其綁定數(shù)據(jù)存入 _bind_params 數(shù)組中。
  • 等待構(gòu)造 sql 完畢后執(zhí)行參數(shù)綁定方法嗡官。
  • 最后執(zhí)行 _execute() 方法執(zhí)行 sql箭窜。

talk is cheap, just show code:

基類添加 _bind_params 屬性衍腥,用于存儲占位符和其綁定數(shù)據(jù)的映射:

protected $_bind_params = [];

添加參數(shù)綁定方法:

protected function _bindParams()
{
    if(is_array($this->_bind_params)) {
        // 將占位符綁定數(shù)據(jù)數(shù)組迭代綁定
        foreach ($this->_bind_params as $plh => $param) {
            // 默認(rèn)為字符串類型
            $data_type = PDO::PARAM_STR;
            // 如果綁定數(shù)據(jù)為數(shù)字
            if(is_numeric($param)) {
                $data_type = PDO::PARAM_INT;
            }
            // 如果綁定數(shù)據(jù)為 null
            if(is_null($param)) {
                $data_type = PDO::PARAM_NULL;
            }
            // 如果綁定數(shù)據(jù)為 Boolean
            if(is_bool($param)) {
                $data_type = PDO::PARAM_BOOL;
            }
            // 執(zhí)行綁定
            $this->_pdoSt->bindValue($plh, $param, $data_type);
        }
    }
}

修改 _execute() 方法:

protected function _execute()
{
    try {
        $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql);
        // 進(jìn)行參數(shù)綁定
        $this->_bindParams();
        $this->_pdoSt->execute();
        $this->_reset();
    } catch (PDOException $e) {
        if($this->_isTimeout($e)) { 

            $this->_closeConnection();
            $this->_connect();
            
            try {
                $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql);
                // 進(jìn)行參數(shù)綁定
                $this->_bindParams();
                $this->_pdoSt->execute();
                $this->_reset();
            } catch (PDOException $e) {
                throw $e;
            }
        } else {
            throw $e;
        }
    }
}

where 方法

現(xiàn)在我們開始開發(fā)條件查詢的主要對外方法 where()

where 方法應(yīng)該包含如下的功能:

  • 構(gòu)造 where 子句字符串磺樱,支持鏈?zhǔn)皆L問
  • 生成占位符,保存占位符和綁定數(shù)據(jù)的映射
  • 支持幾種常用的條件模式 (單條件婆咸、多條件竹捉、是否為 NULL、比較運(yùn)算符的判斷)

1尚骄、構(gòu)造 where 子句字符串

我們希望 where() 方法支持多種條件模式块差,如 where('name', 'jack')、where('age', '<', '30')、where(['name' => 'jack', 'age' => 18])憾儒⊙耍可以觀察得到,方法的參數(shù)是變動的起趾,那么我們可以使用可變參數(shù)诗舰,用 func_num_args() 函數(shù)得到傳入?yún)?shù)的數(shù)量進(jìn)行模式判斷,用 func_get_args() 函數(shù)得到傳入的參數(shù)训裆,這樣就可以實現(xiàn)對多個模式的支持眶根。(當(dāng)然使用可變參數(shù)也是有缺點(diǎn)的,使用可變參數(shù)边琉,接口 ConnectorInterface 中就無法限制該方法參數(shù)的個數(shù)和類型了属百,這里要根據(jù)個人需求取舍)

基類增加 where() 方法:

public function where()
{
    // 多個條件的默認(rèn)連接符為 AND,即與的關(guān)系
    $operator = 'AND';
    // 在一次查詢構(gòu)造過程中变姨,是否是第一次調(diào)用此方法族扰?
    // 在鏈?zhǔn)皆L問中有效
    if($this->_where_str == '') { // 第一次調(diào)用,where 子句需要 WHERE 關(guān)鍵字 
        $this->_where_str = ' WHERE ';
    } else { // 非初次訪問定欧,用連接符拼接上一個條件
        $this->_where_str .= ' '.$operator.' ';
    }
    // 獲取參數(shù)數(shù)量和參數(shù)數(shù)組
    $args_num = func_num_args();
    $params = func_get_args();

    // argurment mode
    switch ($args_num) {
        case 1: // 只有一個參數(shù):傳入數(shù)組渔呵,多條件模式,如 a = b AND c = d ... 默認(rèn) AND 連接
            ...
            break;
        case 2: // 兩個參數(shù):單條件模式
            ...
            break;
        case 3: // 三個參數(shù):比較運(yùn)算符判斷模式
            ...
            break;
        }
    // 實現(xiàn)鏈?zhǔn)讲僮骺仇祷禺?dāng)前實例
    return $this;
}

2扩氢、生成占位符,保存占位符和綁定數(shù)據(jù)的映射

對于 :name 形式的占位符爷辱,只要保證占位符唯一即可录豺。但是如何保證其唯一性呢?占位符不光是在 where 子句中出現(xiàn)饭弓,還在 where in 双饥、where between 這些需要參數(shù)綁定的子句中出現(xiàn)后室。那么按照功能和綁定數(shù)據(jù)拼接字符串來生成嗎烤蜕?但是問題又來了素邪,對于 where值桩,有 where 和 or where 子句,where in 有 where in裹唆、where not in、or where in、or where not in 等組合舍咖,多個要綁定的參數(shù)也可能擁有相同的值,用功能加綁定數(shù)據(jù)拼接字符串來生成占位符很復(fù)雜锉桑,而且因為和方法排霉、參數(shù)本身的依賴度高,沒法獨(dú)立出來民轴,程序的可維護(hù)性也不行攻柠。

當(dāng)然使用 ? 占位符不用考慮那么多 (知名框架 laravel 的查詢構(gòu)造器就是這么做的球订,然而源碼太多,原諒我沒時間看完)瑰钮,但是使用 ? 占位符對參數(shù)綁定的順序有很大的要求冒滩。對于目前我的程序結(jié)構(gòu)來說,_bindParams() 方法只是一股腦的迭代綁定參數(shù)浪谴,并不能分清楚各個參數(shù)的順序开睡,容易導(dǎo)致綁錯數(shù)據(jù)的狀況。

那么苟耻,就說說我最后決定的做法:使用唯一 ID 生成篇恒。

首先將生成占位符的過程獨(dú)立出來,作為一個獨(dú)立方法凶杖,這樣即使以后有了更好的方案胁艰,也不用更改其他程序。

使用 PHP 的 uniqid() 函數(shù)生成一個唯一字符串智蝠,加前綴和熵值 (提高唯一性)腾么,用 MD5 簽一下名 (生成 :name 占位符可接受的字符)。

代碼如下:

// 生成占位符的方法
// 考慮此方法和類實例本身無關(guān)寻咒,所以寫為 static 方法提高效率
protected static function _getPlh() // get placeholder
{
    return ':'.md5(uniqid(mt_rand(), TRUE));
}

性能相關(guān)的思考:

Q:uniqid()哮翘、mt_rand()、md5() 這些函數(shù)的性能如何毛秘?會不會拖慢查詢構(gòu)造器的速度饭寺?

A:這幾個函數(shù)性能不怎么樣,但是就像前言那一篇所說的叫挟,這些函數(shù)的使用的影響是否超過了系統(tǒng)性能的平衡點(diǎn)艰匙?對于此查詢構(gòu)造器程序來講,并不是頻繁使用這些函數(shù)做密集運(yùn)算抹恳,系統(tǒng)的瓶頸還是在和數(shù)據(jù)庫交互的網(wǎng)絡(luò) IO 上员凝,所以,這些函數(shù)是可以使用的奋献。

注:我在一些測試機(jī)上測試過拼接字符串做占位符和隨機(jī) ID 做占位符的壓測 AB 對比健霹,并沒有什么性能差距。當(dāng)然可能在一個處理速度超快的服務(wù)器瓶蚂、數(shù)據(jù)庫組合上能看到差距糖埋,如果各位有更好的方法歡迎提出,對我的方法的不足也歡迎指正窃这。

Q:能保證生成的占位符是唯一的嗎瞳别?

A:如果在多線程的環(huán)境下,存在數(shù)據(jù)競爭,PHP 又沒有好用的線程庫進(jìn)行數(shù)據(jù)加鎖祟敛,會出現(xiàn)重復(fù)的狀況疤坝。但是在其他環(huán)境下不會。傳統(tǒng) web 環(huán)境下每次執(zhí)行隨著一次 HTTP 請求結(jié)束而結(jié)束馆铁,解析 PHP 程序的 PHP-FPM跑揉、MOD_PHP 是多進(jìn)程模型,處理請求時每個進(jìn)程中的數(shù)據(jù)獨(dú)立叼架,互不影響畔裕。而在 workerman 這個常駐內(nèi)存的框架里,多任務(wù)也是一個任務(wù)開啟一個進(jìn)程乖订,數(shù)據(jù)相互獨(dú)立扮饶,每個進(jìn)程中使用 epoll (linux)、select (windows) 來處理并發(fā)乍构,并不會出現(xiàn)并行和數(shù)據(jù)競爭的狀況甜无。所以說只要沒有多線程的需求,則占位符不會重復(fù)哥遮。

3岂丘、支持幾種方便的條件模式

OK,占位符的生成方式搞定眠饮,那么我們開始在 where() 方法中使用吧奥帘。

public function where()
{
    // 多個條件的默認(rèn)連接符為 AND,即與的關(guān)系
    $operator = 'AND';
    // 在一次查詢構(gòu)造過程中仪召,是否是第一次調(diào)用此方法寨蹋?
    // 在鏈?zhǔn)皆L問中有效
    if($this->_where_str == '') { // 第一次調(diào)用,where 子句需要 WHERE 關(guān)鍵字 
        $this->_where_str = ' WHERE ';
    } else { // 非初次訪問扔茅,用連接符拼接上一個條件
        $this->_where_str .= ' '.$operator.' ';
    }
    // 獲取參數(shù)數(shù)量和參數(shù)數(shù)組
    $args_num = func_num_args();
    $params = func_get_args();

    // 判斷傳入的參數(shù)數(shù)量是否合法
    if( ! $args_num || $args_num > 3) {
        throw new \InvalidArgumentException("Error number of parameters");
    }

    // argurment mode
    switch ($args_num) {

        // 只有一個參數(shù):傳入數(shù)組已旧,多條件模式,如 a = b AND c = d ... 默認(rèn) AND 連接
        case 1: 
            if( ! is_array($params[0])) { // 傳入非法參數(shù)召娜,拋出異常提醒
                throw new \InvalidArgumentException($params[0].' should be Array');
            }
            // 遍歷構(gòu)造多條件 where 子句
            $this->_where_str .= '(';
            foreach ($params[0] as $field => $value) {
                $plh = self::_getPlh(); // 生成占位符
                $this->_where_str .= ' '.$field.' = '.$plh.' AND'; // 將占位符添加到子句字符串中
                $this->_bind_params[$plh] = $value; // 保存占位符和待綁定數(shù)據(jù)
            }
            // 清除最后一個 AND 連接符
            $this->_where_str = substr($this->_where_str, 0, strrpos($this->_where_str, 'AND'));
            $this->_where_str .= ')';
            break;

        // 兩個參數(shù):單條件模式
        case 2: 
            if(is_null($params[1])) { // 如果數(shù)據(jù)為 null运褪,則使用 IS NULL 語法
                $this->_where_str .= ' '.$params[0].' IS NULL ';
            } else {
                $plh = self::_getPlh(); // 生成占位符
                $this->_where_str .= ' '.$params[0].' = '.$plh.' '; // 將占位符添加到子句字符串中
                $this->_bind_params[$plh] = $params[1]; // 保存占位符和待綁定數(shù)據(jù)
            }
            break;

        // 三個參數(shù):比較運(yùn)算符判斷模式
        case 3: 
            // 判斷使用的比較運(yùn)算符是否合法 (各數(shù)據(jù)庫的運(yùn)算符支持并不相同)
            if( ! in_array(strtolower($params[1]), $this->_operators)) {
                throw new \InvalidArgumentException('Confusing Symbol '.$params[1]);
            }
            $plh = self::_getPlh(); // 生成占位符
            $this->_where_str .= ' '.$params[0].' '.$params[1].' '.$plh.' '; // 將占位符添加到子句字符串中
            $this->_bind_params[$plh] = $params[2]; // 保存占位符和待綁定數(shù)據(jù)
            break;
        }
    // where 子句構(gòu)造完畢
    return $this;
}

關(guān)于上述代碼這里有幾點(diǎn)要提一下:

在多條件模式下,需要判斷一下傳入的是不是一個數(shù)組 (過濾非法參數(shù)玖瘸,方便開發(fā))秸讹,多個條件之間的連接符默認(rèn)是 AND (大部分多個相等判斷條件之間都是以 AND 的形式連接的,如果有 OR 的需求請用鏈?zhǔn)皆L問的多個 orWhere() 方法)雅倒。

單條件模式下需要判斷綁定數(shù)據(jù)是否為 null璃诀,如果為 null,則使用 IS NULL 的語法 (SQL 中不能用 < > = 判斷 null )屯断。

比較運(yùn)算符判斷模式下文虏,首先要判斷一下傳入的比較運(yùn)算符是否合法,這里各個數(shù)據(jù)庫提供的比較運(yùn)算符是有差異的殖演,所以我們要單獨(dú)設(shè)置一個屬性 _operators 保存這些運(yùn)算符氧秘,Mysql、PostgreSql趴久、Sqlite 這些驅(qū)動類中進(jìn)行重寫丸相。

Mysql 驅(qū)動類中:

// Mysql 提供的比較運(yùn)算符
protected $_operators = [
    '=', '<', '>', '<=', '>=', '<>', '!=', '<=>',
    'like', 'not like', 'like binary', 'rlike', 'regexp', 'not regexp',
    '&', '|', '^', '<<', '>>',
];

PostgreSql 驅(qū)動類中:

// PostgreSql 提供的比較運(yùn)算符
protected $_operators = [
    '=', '<', '>', '<=', '>=', '<>', '!=',
    'like', 'not like', 'ilike', 'similar to', 'not similar to',
    '&', '|', '#', '<<', '>>',
];

Sqlite 驅(qū)動類中:

// Sqlite 提供的比較運(yùn)算符
protected $_operators = [
    '=', '<', '>', '<=', '>=', '<>', '!=',
    'like', 'not like', 'ilike',
    '&', '|', '<<', '>>',
];

測試

打開 test/test.php,修改代碼:

require_once dirname(dirname(__FILE__)) . '/vendor/autoload.php';

use Drivers\Mysql;

$config = [
    'host'        => 'localhost',
    'port'        => '3306',
    'user'        => 'username',
    'password'    => 'password',
    'dbname'      => 'database',
    'charset'     => 'utf8',
    'timezone'    => '+8:00',
    'collection'  => 'utf8_general_ci',
    'strict'      => false,
];

$driver = new Mysql($config);

// 單條件模式測試
$results = $driver->table('test_table')
                  ->select('*')
                  ->where('username', 'jack')
                  ->get();

var_dump($results);

// 鏈?zhǔn)皆L問 + 比較運(yùn)算符判斷模式測試
$results = $driver->table('test_table')
                  ->select('*')
                  ->where('username', 'jack')
                  ->where('age', '<', 30)
                  ->get();

var_dump($results);

// 多條件模式測試
$results = $driver->table('test_table')
                  ->select('*')
                  ->where([
                      'username' => 'jack',
                      'age' => 18,
                  ])
                  ->get();

var_dump($results);

執(zhí)行看看彼棍,數(shù)據(jù)是不是如你所想灭忠。

優(yōu)化

雖然完成了 where() 方法的編寫,但是我們發(fā)現(xiàn) where() 方法中的代碼很臃腫座硕,而且后續(xù)編寫 orWhere()弛作、having() 這些都需要用到條件查詢的方法時,很多的代碼都是重復(fù)的华匾。既然如此映琳,那么就把這部分代碼提出來。

基類添加 _condition_constructor 方法:

// $args_num 為 where() 傳入?yún)?shù)的數(shù)量
// $params 為 where() 傳入的參數(shù)數(shù)組
// $construct_str 為要構(gòu)造的子句的字符串蜘拉,在 where() 方法中調(diào)用會傳入 $this->_where_str 
// 因為要改變該子句字符串萨西,所以這里使用引用傳遞
protected function _condition_constructor($args_num, $params, &$construct_str)
{
    if( ! $args_num || $args_num > 3) {
        throw new \InvalidArgumentException("Error number of parameters");
    }

    switch ($args_num) {
        case 1: 
            if( ! is_array($params[0])) {
                throw new \InvalidArgumentException($params[0].' should be Array');
            }
            $construct_str .= '(';
            foreach ($params[0] as $field => $value) {
                $plh = self::_getPlh();
                $construct_str .= ' '.$field.' = '.$plh.' AND';
                $this->_bind_params[$plh] = $value;
            }
            $construct_str = substr($construct_str, 0, strrpos($construct_str, 'AND'));
            $construct_str .= ')';
            break;
        case 2: 
            if(is_null($params[1])) {
                $construct_str .= ' '.$params[0].' IS NULL ';
            } else {
                $plh = self::_getPlh();
                $construct_str .= ' '.$params[0].' = '.$plh.' ';
                $this->_bind_params[$plh] = $params[1];
            }
            break;
        case 3: 
            if( ! in_array(strtolower($params[1]), $this->_operators)) {
                throw new \InvalidArgumentException('Confusing Symbol '.$params[1]);
            }
            $plh = self::_getPlh();
            $construct_str .= ' '.$params[0].' '.$params[1].' '.$plh.' ';
            $this->_bind_params[$plh] = $params[2];
            break;
    }

}

修改后的 where() 方法:

public function where()
{
    // 多個條件的默認(rèn)連接符為 AND,即與的關(guān)系
    $operator = 'AND';
    // 在一次查詢構(gòu)造過程中旭旭,是否是第一次調(diào)用此方法谎脯?
    // 在鏈?zhǔn)皆L問中有效
    if($this->_where_str == '') { // 第一次調(diào)用,where 子句需要 WHERE 關(guān)鍵字 
        $this->_where_str = ' WHERE ';
    } else { // 非初次訪問持寄,用連接符拼接上一個條件
        $this->_where_str .= ' '.$operator.' ';
    }
    // 進(jìn)行占位符生成源梭、參數(shù)綁定、生成子句字符串操作
    $this->_condition_constructor(func_num_args(), func_get_args(), $this->_where_str);

    return $this;
}

這樣我們就把可以通用的邏輯提出來了际看,趁熱打鐵咸产,我們把 orWhere() 方法也添加到基類中。

對于 orWhere() 方法仲闽,和 where() 方法的區(qū)別只有鏈?zhǔn)讲僮鬟M(jìn)行多條件查詢時的連接符不同:

public function orWhere()
{
    $operator = 'OR';
    
    if($this->_where_str == '') {
        $this->_where_str = ' WHERE ';
    } else {
        $this->_where_str .= ' '.$operator.' ';
    }
    
    $this->_condition_constructor(func_num_args(), func_get_args(), $this->_where_str);

    return $this;
}

構(gòu)造語句 SELECT * FROM test_table WHERE username = 'jack' OR username = 'mike';:

$results = $driver->table('test_table')
                  ->select('*')
                  ->where('username', 'jack')
                  ->orWhere('username', 'mike')
                  ->get();

關(guān)鍵字沖突

熟悉數(shù)據(jù)庫的朋友們應(yīng)該知道脑溢,每種數(shù)據(jù)庫都有一些關(guān)鍵字,一部分是 SQL 語句的關(guān)鍵字赖欣,另一部分是數(shù)據(jù)庫自己的關(guān)鍵字屑彻。既然有關(guān)鍵字,那么就避免不了用戶鍵入的數(shù)據(jù)和關(guān)鍵字重名的問題顶吮,比如表名和關(guān)鍵字重名社牲、字段名 (別名) 和關(guān)鍵字重名等。

那么如何解決關(guān)鍵字沖突呢悴了?

當(dāng)然搏恤,建表的時候盡量注意命名违寿,不要和關(guān)鍵字沖突是一種方法,但是如果這個表的建立熟空、修改權(quán)限不在你手中藤巢,你又要訪問這個表去拿數(shù)據(jù)的時候就沒招了,所以我們常常要對歷史遺留問題進(jìn)行兼容處理息罗。

各數(shù)據(jù)庫使用了類似轉(zhuǎn)義的做法掂咒。Mysql 使用反引號 ` 來包裹字符串避免數(shù)據(jù)庫將這個字符解析為關(guān)鍵字,PostgreSql 和 Sqlite 則是用雙引號 " 來做相應(yīng)的工作迈喉。

而在使用查詢構(gòu)造器的過程中绍刮,總不能每次由用戶手動來寫這個符號 (如 where('`count`', 12) ),這樣更換數(shù)據(jù)庫驅(qū)動的時候會影響到上層的代碼挨摸,可維護(hù)性差 (如 mysql 切到 pgsql孩革,需要把所有 ` 改為 " )。所以得运,為可能出現(xiàn)關(guān)鍵字沖突的地方添加引號應(yīng)該交給查詢構(gòu)造器底層去做嫉戚。

既然各個數(shù)據(jù)庫有差異,想必現(xiàn)在大家已經(jīng)知道該怎么做了澈圈,基類添加屬性 _quote_symbol彬檀,Mysql 類中進(jìn)行重寫。

Mysql 驅(qū)動類中添加:

// 因為次屬性不會改變瞬女,使用 static 關(guān)鍵字
protected static $_quote_symbol = '`';

PostgreSql 和 Sqlite 同理窍帝,這里不單獨(dú)演示了。

下面我們給基類添加 _quote() 方法诽偷,用于給字符串添加引號:

// static 方法
protected static function _quote($word)
{
    return static::$_quote_symbol.$word.static::$_quote_symbol;
}

有了這個方法坤学,我們可以簡單的防止一個字符串關(guān)鍵字沖突了。但是在實際應(yīng)用中還遠(yuǎn)不夠报慕。

首先深浮,在書寫 SQL 時字段的表述有很多模式

  • 別名:username as name (這里不對省略 as 的情況做處理,請不要省略 as 關(guān)鍵字)
  • 點(diǎn)號:table_name.field
  • SQL 函數(shù)做為列:COUNT(field)

我們必須的對這些常用情形做處理眠冈,而不只是直接對這些字符串的兩邊加引號飞苇。

對字符串的匹配處理,那么我們首先想到的是正則表達(dá)式蜗顽。至于正則的性能布卡,還是參考前言所說的性能平衡點(diǎn),這里每次請求用到正則的次數(shù)很少雇盖,并沒有突破數(shù)據(jù)庫連接和執(zhí)行的網(wǎng)絡(luò) IO 瓶頸忿等,所以可以使用。

在基類添加 _wrapRow 方法崔挖,用來處理 SQL 字段的字符串:

// static 方法
protected static function _wrapRow($str)
{
    // 匹配模式
    $alias_pattern = '/([a-zA-Z0-9_\.]+)\s+(AS|as|As)\s+([a-zA-Z0-9_]+)/';
    $alias_replace = self::_quote('$1').' $2 '.self::_quote('$3');
    $prefix_pattern = '/([a-zA-Z0-9_]+\s*)(\.)(\s*[a-zA-Z0-9_]+)/';
    $prefix_replace = self::_quote('$1').'$2'.self::_quote('$3');
    $func_pattern = '/[a-zA-Z0-9_]+\([a-zA-Z0-9_\,\s\`\'\"\*]*\)/';
    // alias mode 別名模式
    if(preg_match($alias_pattern, $str, $alias_match)) {
        // 如果列是 aa.bb as cc 的模式
        if(preg_match($prefix_pattern, $alias_match[1])) {
            $pre_rst = preg_replace($prefix_pattern, $prefix_replace, $alias_match[1]);
            $alias_replace = $pre_rst.' $2 '.self::_quote('$3');
        }
        // 如果列是 aa as bb 的模式
        return preg_replace($alias_pattern, $alias_replace, $str);
    }
    // prefix mode 表.字段 模式
    if(preg_match($prefix_pattern, $str)) {
        return preg_replace($prefix_pattern, $prefix_replace, $str);
    }
    // func mode 函數(shù)模式贸街,什么都不做庵寞,交給用戶去處理
    if(preg_match($func_pattern, $str)) {
        return $str;
    }
    // field mode 簡單的字段模式,直接加引號返回
    return self::_quote($str);
}

上訴代碼有幾點(diǎn)要說明:

別名模式是最復(fù)雜的薛匪,需要判斷是 aa as bb 模式還是 aa.bb as cc 模式皇帮,匹配替換后的結(jié)果是 `aa` as `bb` 、`aa`.`bb` as `cc` (這里以 mysql 為例)蛋辈。

函數(shù)模式如 count(aa.cc)、max(count) 這種将谊,函數(shù)的參數(shù)數(shù)量不定冷溶,模式多變不好匹配,交給用戶手動輸入原生字符串去處理尊浓,而且諸如此類的聚合函數(shù)的話逞频,后面的篇幅會增加聚合函數(shù)的相關(guān)方法去獲得結(jié)果。

有了 _wrapRow() 方法栋齿,我們可以使關(guān)鍵字沖突的處理對上層應(yīng)用完全透明苗胀。

修改 table() 方法:

public function table($table)
{
    // 添加引號
    $this->_table = self::_wrapRow($table);

    return $this;
}

修改 select() 方法:

public function select()
{
    $cols = func_get_args();

    if( ! func_num_args() || in_array('*', $cols)) {
        $this->_cols_str = ' * ';
    } else {
        $this->_cols_str = '';
        foreach ($cols as $col) {
            // 添加引號
            $this->_cols_str .= ' '.self::_wrapRow($col).',';
        }
        $this->_cols_str = rtrim($this->_cols_str, ',');
    }

    return $this;
}

修該用于條件構(gòu)造的 _condition_constructor() 方法:

protected function _condition_constructor($args_num, $params, &$construct_str)
{
    if( ! $args_num || $args_num > 3) {
        throw new \InvalidArgumentException("Error number of parameters");
    }

    switch ($args_num) {
        case 1: 
            if( ! is_array($params[0])) {
                throw new \InvalidArgumentException($params[0].' should be Array');
            }
            $construct_str .= '(';
            foreach ($params[0] as $field => $value) {
                $plh = self::_getPlh();
                // 添加引號
                $construct_str .= ' '.self::_wrapRow($field).' = '.$plh.' AND';
                $this->_bind_params[$plh] = $value;
            }
            
            $construct_str = substr($construct_str, 0, strrpos($construct_str, 'AND'));
            $construct_str .= ')';
            break;
        case 2: 
            if(is_null($params[1])) {
                // 添加引號
                $construct_str .= ' '.self::_wrapRow($params[0]).' IS NULL ';
            } else {
                $plh = self::_getPlh();
                // 添加引號
                $construct_str .= ' '.self::_wrapRow($params[0]).' = '.$plh.' ';
                $this->_bind_params[$plh] = $params[1];
            }
            break;
        case 3: 
            if( ! in_array(strtolower($params[1]), $this->_operators)) {
                throw new \InvalidArgumentException('Confusing Symbol '.$params[1]);
            }
            $plh = self::_getPlh();
            // 添加引號
            $construct_str .= ' '.self::_wrapRow($params[0]).' '.$params[1].' '.$plh.' ';
            $this->_bind_params[$plh] = $params[2];
            break;
    }

}

現(xiàn)在我們給要查的數(shù)據(jù)表中添加一個名為 group 的字段,構(gòu)造一下 SELECT * FROM test_table where group = 'test'; 這個語句瓦堵,看是否會報錯呢基协?

$results = $driver->table('test_table')
    ->select('*')
    ->where('group', 'test')
    ->get();

Just do it!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市菇用,隨后出現(xiàn)的幾起案子澜驮,更是在濱河造成了極大的恐慌,老刑警劉巖惋鸥,帶你破解...
    沈念sama閱讀 206,378評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杂穷,死亡現(xiàn)場離奇詭異,居然都是意外死亡卦绣,警方通過查閱死者的電腦和手機(jī)耐量,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來滤港,“玉大人廊蜒,你說我怎么就攤上這事〗ρ” “怎么了劲藐?”我有些...
    開封第一講書人閱讀 152,702評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長樟凄。 經(jīng)常有香客問我聘芜,道長,這世上最難降的妖魔是什么缝龄? 我笑而不...
    開封第一講書人閱讀 55,259評論 1 279
  • 正文 為了忘掉前任汰现,我火速辦了婚禮挂谍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘瞎饲。我一直安慰自己口叙,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評論 5 371
  • 文/花漫 我一把揭開白布嗅战。 她就那樣靜靜地躺著妄田,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驮捍。 梳的紋絲不亂的頭發(fā)上疟呐,一...
    開封第一講書人閱讀 49,036評論 1 285
  • 那天,我揣著相機(jī)與錄音东且,去河邊找鬼启具。 笑死,一個胖子當(dāng)著我的面吹牛珊泳,可吹牛的內(nèi)容都是我干的鲁冯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,349評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼色查,長吁一口氣:“原來是場噩夢啊……” “哼薯演!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起秧了,我...
    開封第一講書人閱讀 36,979評論 0 259
  • 序言:老撾萬榮一對情侶失蹤涣仿,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后示惊,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體好港,經(jīng)...
    沈念sama閱讀 43,469評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評論 2 323
  • 正文 我和宋清朗相戀三年米罚,在試婚紗的時候發(fā)現(xiàn)自己被綠了钧汹。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,059評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡录择,死狀恐怖拔莱,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情隘竭,我是刑警寧澤塘秦,帶...
    沈念sama閱讀 33,703評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站动看,受9級特大地震影響尊剔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜菱皆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評論 3 307
  • 文/蒙蒙 一须误、第九天 我趴在偏房一處隱蔽的房頂上張望挨稿。 院中可真熱鬧,春花似錦京痢、人聲如沸奶甘。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽臭家。三九已至,卻和暖如春方淤,著一層夾襖步出監(jiān)牢的瞬間钉赁,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工臣淤, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人窃爷。 一個月前我還...
    沈念sama閱讀 45,501評論 2 354
  • 正文 我出身青樓邑蒋,卻偏偏與公主長得像,于是被迫代替她去往敵國和親按厘。 傳聞我的和親對象是個殘疾皇子医吊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評論 2 345

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)逮京,斷路器卿堂,智...
    卡卡羅2017閱讀 134,599評論 18 139
  • 轉(zhuǎn) # https://www.cnblogs.com/easypass/archive/2010/12/ 08/...
    呂品?閱讀 9,698評論 0 44
  • 一棵大樹被鋸倒了和我沒有任何關(guān)系 那是鄰居家的一棵樹一棵白楊樹已經(jīng)好多年了,在他家院里遮天弊日 就在今晨一陣油鋸的...
    王春淶閱讀 687評論 12 32
  • 蓮 池 何少波 蓮池荷池蓮菜池懒棉, 荷花初開立作詩草描。 敢問蜻蜓波何許, 又問鯉鯽幾時須策严。 (2018年3月...
    南塬牛閱讀 175評論 0 3
  • 人生不過短短的900個月妻导,畫一個30×30的表格逛绵,一張A4紙就足夠了, 如果每過一個月倔韭,把一個格子涂掉术浪, 全部人生...
    劍飛先森閱讀 176評論 0 4