構(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!