亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

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

why_rookie / 2843人閱讀

摘要:構(gòu)造條件如果單單是執(zhí)行這樣的語(yǔ)句,使用原生擴(kuò)展就好了,使用查詢構(gòu)造器就是殺雞用牛刀。這一篇,我們來(lái)講講如何使用查詢構(gòu)造器進(jìn)行條件查詢。

構(gòu)造 where 條件

如果單單是執(zhí)行 SELECT * FROM test_table; 這樣的語(yǔ)句,使用原生擴(kuò)展就好了,使用查詢構(gòu)造器就是殺雞用牛刀。當(dāng)然,在實(shí)際的業(yè)務(wù)需求中,大部分的 SQL 都沒(méi)這么簡(jiǎn)單,有各種條件查詢、分組、排序、連表等操作,尤其是條件查詢,占到了查詢業(yè)務(wù)的大多數(shù)。

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

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

首先,我們回顧一下用 PDO 來(lái)寫(xiě)條件查詢?cè)撛趺醋觯?/p>

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

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

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

2、進(jìn)行參數(shù)綁定,執(zhí)行語(yǔ)句:

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

$username = "test";
$age = 18;

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

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

占位符和綁定方法的選擇

占位符選擇:

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

綁定方法的選擇:

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

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

這里我們選擇 PDOStatement::bindValue() 方法,因?yàn)閰?shù)綁定過(guò)程和 execute 執(zhí)行過(guò)程可能被封裝到不同的方法中,我們需要簡(jiǎn)單的傳值傳遞而不是引用傳遞。

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

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

當(dāng)然,并非只有 where 子句需要參數(shù)綁定,having 子句、where in 子句等也涉及到參數(shù)的綁定。為了程序結(jié)構(gòu)的靈活和清晰,我們?cè)诨愋录右粋€(gè) _bind_params 屬性,鍵值數(shù)組類型,用來(lái)存儲(chǔ)占位符和其綁定數(shù)據(jù)的映射。這樣,我們只需:

構(gòu)造 where (having 等) 子句字符串時(shí)生成占位符,并將占位符和其綁定數(shù)據(jù)存入 _bind_params 數(shù)組中。

等待構(gòu)造 sql 完畢后執(zhí)行參數(shù)綁定方法。

最后執(zhí)行 _execute() 方法執(zhí)行 sql。

talk is cheap, just show code:

基類添加 _bind_params 屬性,用于存儲(chǔ)占位符和其綁定數(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)在我們開(kāi)始開(kāi)發(fā)條件查詢的主要對(duì)外方法 where()

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

構(gòu)造 where 子句字符串,支持鏈?zhǔn)皆L問(wèn)

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

支持幾種常用的條件模式 (單條件、多條件、是否為 NULL、比較運(yùn)算符的判斷)

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

我們希望 where() 方法支持多種條件模式,如 where("name", "jack")、where("age", "<", "30")、where(["name" => "jack", "age" => 18])??梢杂^察得到,方法的參數(shù)是變動(dòng)的,那么我們可以使用可變參數(shù),用 func_num_args() 函數(shù)得到傳入?yún)?shù)的數(shù)量進(jìn)行模式判斷,用 func_get_args() 函數(shù)得到傳入的參數(shù),這樣就可以實(shí)現(xiàn)對(duì)多個(gè)模式的支持。(當(dāng)然使用可變參數(shù)也是有缺點(diǎn)的,使用可變參數(shù),接口 ConnectorInterface 中就無(wú)法限制該方法參數(shù)的個(gè)數(shù)和類型了,這里要根據(jù)個(gè)人需求取舍)

基類增加 where() 方法:

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

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

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

對(duì)于 :name 形式的占位符,只要保證占位符唯一即可。但是如何保證其唯一性呢?占位符不光是在 where 子句中出現(xiàn),還在 where in 、where between 這些需要參數(shù)綁定的子句中出現(xiàn)。那么按照功能和綁定數(shù)據(jù)拼接字符串來(lái)生成嗎?但是問(wèn)題又來(lái)了,對(duì)于 where,有 where 和 or where 子句,where in 有 where in、where not in、or where in、or where not in 等組合,多個(gè)要綁定的參數(shù)也可能擁有相同的值,用功能加綁定數(shù)據(jù)拼接字符串來(lái)生成占位符很復(fù)雜,而且因?yàn)楹头椒?、參?shù)本身的依賴度高,沒(méi)法獨(dú)立出來(lái),程序的可維護(hù)性也不行。

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

那么,就說(shuō)說(shuō)我最后決定的做法:使用唯一 ID 生成。

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

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

代碼如下:

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

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

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

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

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

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

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

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

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

public function where()
{
    // 多個(gè)條件的默認(rèn)連接符為 AND,即與的關(guān)系
    $operator = "AND";
    // 在一次查詢構(gòu)造過(guò)程中,是否是第一次調(diào)用此方法?
    // 在鏈?zhǔn)皆L問(wèn)中有效
    if($this->_where_str == "") { // 第一次調(diào)用,where 子句需要 WHERE 關(guān)鍵字 
        $this->_where_str = " WHERE ";
    } else { // 非初次訪問(wèn),用連接符拼接上一個(gè)條件
        $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) {

        // 只有一個(gè)參數(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ù)
            }
            // 清除最后一個(gè) AND 連接符
            $this->_where_str = substr($this->_where_str, 0, strrpos($this->_where_str, "AND"));
            $this->_where_str .= ")";
            break;

        // 兩個(gè)參數(shù):?jiǎn)螚l件模式
        case 2: 
            if(is_null($params[1])) { // 如果數(shù)據(jù)為 null,則使用 IS NULL 語(yǔ)法
                $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;

        // 三個(gè)參數(shù):比較運(yùn)算符判斷模式
        case 3: 
            // 判斷使用的比較運(yùn)算符是否合法 (各數(shù)據(jù)庫(kù)的運(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)要提一下:

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

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

比較運(yùn)算符判斷模式下,首先要判斷一下傳入的比較運(yùn)算符是否合法,這里各個(gè)數(shù)據(jù)庫(kù)提供的比較運(yùn)算符是有差異的,所以我們要多帶帶設(shè)置一個(gè)屬性 _operators 保存這些運(yùn)算符,Mysql、PostgreSql、Sqlite 這些驅(qū)動(dòng)類中進(jìn)行重寫(xiě)。

Mysql 驅(qū)動(dòng)類中:

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

PostgreSql 驅(qū)動(dòng)類中:

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

Sqlite 驅(qū)動(dòng)類中:

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

打開(kāi) test/test.php,修改代碼:

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

use DriversMysql;

$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);

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

var_dump($results);

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

var_dump($results);

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

var_dump($results);

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

優(yōu)化

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

基類添加 _condition_constructor 方法:

// $args_num 為 where() 傳入?yún)?shù)的數(shù)量
// $params 為 where() 傳入的參數(shù)數(shù)組
// $construct_str 為要構(gòu)造的子句的字符串,在 where() 方法中調(diào)用會(huì)傳入 $this->_where_str 
// 因?yàn)橐淖冊(cè)撟泳渥址?,所以這里使用引用傳遞
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()
{
    // 多個(gè)條件的默認(rèn)連接符為 AND,即與的關(guān)系
    $operator = "AND";
    // 在一次查詢構(gòu)造過(guò)程中,是否是第一次調(diào)用此方法?
    // 在鏈?zhǔn)皆L問(wèn)中有效
    if($this->_where_str == "") { // 第一次調(diào)用,where 子句需要 WHERE 關(guān)鍵字 
        $this->_where_str = " WHERE ";
    } else { // 非初次訪問(wèn),用連接符拼接上一個(gè)條件
        $this->_where_str .= " ".$operator." ";
    }
    // 進(jìn)行占位符生成、參數(shù)綁定、生成子句字符串操作
    $this->_condition_constructor(func_num_args(), func_get_args(), $this->_where_str);

    return $this;
}

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

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

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)造語(yǔ)句 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ù)庫(kù)的朋友們應(yīng)該知道,每種數(shù)據(jù)庫(kù)都有一些關(guān)鍵字,一部分是 SQL 語(yǔ)句的關(guān)鍵字,另一部分是數(shù)據(jù)庫(kù)自己的關(guān)鍵字。既然有關(guān)鍵字,那么就避免不了用戶鍵入的數(shù)據(jù)和關(guān)鍵字重名的問(wèn)題,比如表名和關(guān)鍵字重名、字段名 (別名) 和關(guān)鍵字重名等。

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

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

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

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

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

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

// 因?yàn)榇螌傩圆粫?huì)改變,使用 static 關(guān)鍵字
protected static $_quote_symbol = "`";

PostgreSql 和 Sqlite 同理,這里不多帶帶演示了。

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

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

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

首先,在書(shū)寫(xiě) SQL 時(shí)字段的表述有很多模式

別名:username as name (這里不對(duì)省略 as 的情況做處理,請(qǐng)不要省略 as 關(guān)鍵字)

點(diǎn)號(hào):table_name.field

SQL 函數(shù)做為列:COUNT(field)

我們必須的對(duì)這些常用情形做處理,而不只是直接對(duì)這些字符串的兩邊加引號(hào)。

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

在基類添加 _wrapRow 方法,用來(lái)處理 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 簡(jiǎn)單的字段模式,直接加引號(hào)返回
    return self::_quote($str);
}

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

別名模式是最復(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ù)量不定,模式多變不好匹配,交給用戶手動(dòng)輸入原生字符串去處理,而且諸如此類的聚合函數(shù)的話,后面的篇幅會(huì)增加聚合函數(shù)的相關(guān)方法去獲得結(jié)果。

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

修改 table() 方法:

public function table($table)
{
    // 添加引號(hào)
    $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) {
            // 添加引號(hào)
            $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();
                // 添加引號(hào)
                $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])) {
                // 添加引號(hào)
                $construct_str .= " ".self::_wrapRow($params[0])." IS NULL ";
            } else {
                $plh = self::_getPlh();
                // 添加引號(hào)
                $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();
            // 添加引號(hào)
            $construct_str .= " ".self::_wrapRow($params[0])." ".$params[1]." ".$plh." ";
            $this->_bind_params[$plh] = $params[2];
            break;
    }

}

現(xiàn)在我們給要查的數(shù)據(jù)表中添加一個(gè)名為 group 的字段,構(gòu)造一下 SELECT * FROM test_table where group = "test"; 這個(gè)語(yǔ)句,看是否會(huì)報(bào)錯(cuò)呢?

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

Just do it!

文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/30828.html

相關(guān)文章

  • 寫(xiě)一個(gè)特殊查詢構(gòu)造器 - (前言)

    摘要:而在項(xiàng)目開(kāi)發(fā)中,我們想要的是一個(gè)更好用的可維護(hù)的工具,此時(shí),對(duì)代碼的封裝模塊化就顯得尤為重要,于是出現(xiàn)了兩種方案查詢構(gòu)造器,對(duì)象關(guān)系映射。典型環(huán)境下按照一般的查詢構(gòu)造器處理就行。 文章目錄 寫(xiě)一個(gè)特殊的查詢構(gòu)造器 - (前言) 寫(xiě)一個(gè)特殊的查詢構(gòu)造器 - (一、程序結(jié)構(gòu),基礎(chǔ)封裝) 寫(xiě)一個(gè)特殊的查詢構(gòu)造器 - (二、第一條語(yǔ)句) 寫(xiě)一個(gè)特殊的查詢構(gòu)造器 - (三、條件查詢) 寫(xiě)一個(gè)特殊...

    GitChat 評(píng)論0 收藏0
  • 寫(xiě)一個(gè)特殊查詢構(gòu)造器 - (四、條件查詢:復(fù)雜條件)

    摘要:復(fù)雜的條件在的條件查詢中,不只有這些基本的子句,還有等復(fù)雜一些的子句。這篇我們就來(lái)講一下查詢構(gòu)造器如何構(gòu)造這些復(fù)雜的查詢語(yǔ)句。 復(fù)雜的條件 在 SQL 的條件查詢中,不只有 where、or where 這些基本的子句,還有 where in、where exists、where between 等復(fù)雜一些的子句。而且即使是 where 這種基礎(chǔ)的子句,也有多個(gè)條件的多種邏輯組合。這篇...

    baoxl 評(píng)論0 收藏0
  • 寫(xiě)一個(gè)特殊查詢構(gòu)造器 - (六、關(guān)聯(lián))

    摘要:雖然現(xiàn)在這樣的情況已經(jīng)很少,但是對(duì)于查詢構(gòu)造器而言,還是要提供一個(gè)方便的方法來(lái)對(duì)表前綴進(jìn)行設(shè)置,特別是當(dāng)你沒(méi)有權(quán)限修改表名的時(shí)候。所以我們將表前綴作為一個(gè)配置參數(shù)傳入查詢構(gòu)造器,在查詢構(gòu)造器的底層進(jìn)行自動(dòng)前綴添加。 關(guān)聯(lián)查詢是關(guān)系型數(shù)據(jù)庫(kù)典型的查詢語(yǔ)句,根據(jù)兩個(gè)或多個(gè)表中的列之間的關(guān)系,從這些表中查詢數(shù)據(jù)。在 SQL 標(biāo)準(zhǔn)中使用 JOIN 和 ON 關(guān)鍵字來(lái)實(shí)現(xiàn)關(guān)聯(lián)查詢。 Join 子...

    rainyang 評(píng)論0 收藏0
  • 寫(xiě)一個(gè)特殊查詢構(gòu)造器 - (五、聚合函數(shù)、分組、排序、分頁(yè))

    摘要:聚合函數(shù)在中,有一些用來(lái)統(tǒng)計(jì)匯總的函數(shù),被稱作聚合函數(shù),如等。方法其它方法如之類的編寫(xiě)就不一一展示了,代碼請(qǐng)看聚合函數(shù)。如何獲取總數(shù)當(dāng)然是使用上面講到的聚合函數(shù)來(lái)處理。 where 相關(guān)的子句構(gòu)造完成后,我們繼續(xù)構(gòu)造其它子句。這一篇我們進(jìn)行聚合函數(shù)、分組、排序等子句的構(gòu)造。 聚合函數(shù) 在 SQL 中,有一些用來(lái)統(tǒng)計(jì)、匯總的函數(shù),被稱作聚合函數(shù),如 SUM、COUNT、AVG 等。 使用...

    iamyoung001 評(píng)論0 收藏0
  • 寫(xiě)一個(gè)特殊查詢構(gòu)造器 - (二、第一條語(yǔ)句)

    摘要:注在常駐內(nèi)存單例模式下,這種多次用一個(gè)類進(jìn)行查詢的情形很常見(jiàn)。斷線重連對(duì)于典型環(huán)境而言,一次的查詢已經(jīng)隨著的請(qǐng)求而結(jié)束,的垃圾回收功能會(huì)回收一次請(qǐng)求周期內(nèi)的數(shù)據(jù)。但在常駐內(nèi)存的環(huán)境下,尤其是單例模式下,數(shù)據(jù)庫(kù)驅(qū)動(dòng)類可能一直在內(nèi)存中不被銷毀。 構(gòu)造、執(zhí)行第一條語(yǔ)句 上一篇完成了代碼結(jié)構(gòu)的搭建和 PDO 的基礎(chǔ)封裝,這一篇我們來(lái)講如何構(gòu)造一個(gè)最基本的 SQL 語(yǔ)句,并執(zhí)行得到結(jié)果。 que...

    dadong 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<