上一篇完成了代碼結構的搭建和 PDO 的基礎封裝,這一篇咱們來說如何構造一個最基本的 SQL 語句,並執行獲得結果。php
query sql 構造目標: SELECT * FROM test_table;
html
查詢構造器執行語法構造目標: $drivers->table('test_table')->select('\*')->get();
mysql
測試用的數據表請你們本身創建,這裏就不單獨演示了。git
咱們回顧下 PDO 執行這個 query 語句的基本用法:github
一、PDO::query() 方法獲取結果集:web
$pdo->query("SELECT * FROM test_table;");
二、PDO::prepare()、PDOStatement::execute() 方法:sql
$pdoSt = $pdo->prepare("SELECT * FROM test_table;"); $pdoSt->execute(); $pdoSt->fetchAll(PDO::FETCH_ASSOC);
PDO::prepare() 方法提供了防注入、參數綁定的機制,能夠指定結果集的返回格式,更加靈活易於封裝,咱們選這種。數據庫
要構造 query sql 語句,那麼不妨先觀察一下它的基本構造:數組
SELECT、 要查找的字段(列)、 FROM、 要查找的表、 關聯子句、 條件子句、 分組子句、 排序子句、 LIMIT 子句。
除了 SELECT 和 FROM 是固定不變,咱們只需構造好查詢字段、表名和一系列子句的字符串,而後按照 query sql 的語法拼接在一塊兒便可。服務器
在基類 PDODriver.php 中添加屬性做爲構造字符串:
protected $_table = ''; // table 名 protected $_prepare_sql = ''; // prepare 方法執行的 sql 語句 protected $_cols_str = ' * '; // 須要查詢的字段,默認爲 * (所有) protected $_where_str = ''; // where 子句 protected $_orderby_str = ''; // order by 子句 protected $_groupby_str = ''; // group by 子句 protected $_having_str = ''; // having 子句 (配合 group by 使用) protected $_join_str = ''; // join 子句 protected $_limit_str = ''; // limit 子句
有了基本的構造字符串屬性,能夠開始構造一條 sql 了。
添加 _buildQuery() 方法,用來構造 sql 字符串:
protected function _buildQuery() { $this->_prepare_sql = 'SELECT '.$this->_cols_str.' FROM '.$this->_table. $this->_join_str. $this->_where_str. $this->_groupby_str.$this->_having_str. $this->_orderby_str. $this->_limit_str; }
添加 table() 方法,用來設置表名:
public function table($table) { $this->_table = $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 = ''; // 清除默認的 * 值 // 構造 "field1, filed2 ..." 字符串 foreach ($cols as $col) { $this->_cols_str .= ' '.$col.','; } $this->_cols_str = rtrim($this->_cols_str, ','); } return $this; }
sql 字符串構造完畢,接下來就須要一個執行 sql 並取得結果的方法來收尾。
添加 get() 方法:
public function get() { try { $this->_buildQuery(); // 構建 sql // prepare 預處理 $pdoSt = $this->_pdo->prepare($this->_prepare_sql); // 執行 $pdoSt->execute(); } catch (PDOException $e) { throw $e; } return $pdoSt->fetchAll(PDO::FETCH_ASSOC); // 獲取一個以鍵值數組形式的結果集 }
修改 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); // 執行 SELECT * FROM test_table; 的查詢 $results = $driver->table('test_table')->select('*')->get(); var_dump($results);
注:上述代碼中因爲 _cols_str 屬性默認爲 ' * ',因此在查詢所有字段時省略 select() 方法的調用也是能夠的。
以後爲了節省篇幅,一些通用的方法只使用 Mysql 驅動類做爲測試對象,PostgreSql 和 Sqlite 請讀者本身進行測試,以後不會再單獨說明。
get 方法中的 prepare、execute 過程是通用的 (查詢、插入、刪除、更新等操做),咱們能夠將這部分代碼提出來,在其它執行 sql 取結果的方法中複用。
基類中新建 _execute() 方法:
protected function _execute() { try { $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_pdoSt->execute(); } catch (PDOException $e) { throw $e; } }
因爲將邏輯分離到另外一個方法中,get() 方法獲取不到 PDOStatement 實例,所以將 PDOStatement 實例保存到基類的屬性中:
protected $_pdoSt = NULL;
修改後的 get() 方法:
public function get() { $this->_buildQuery(); $this->_execute(); return $this->_pdoSt->fetchAll(PDO::FETCH_ASSOC); }
使用查詢構造器一次查詢後,各個構造字符串的內容已經被修改,爲了避免影響下一次查詢,須要將這些構造字符串恢復到初始狀態。
注:在常駐內存單例模式下,這種屢次用一個類進行查詢的情形很常見。
添加 _reset() 方法:
protected function _reset() { $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 = ''; $this->_bind_params = []; }
修改 _execute() 方法:
protected function _execute() { try { $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_pdoSt->execute(); $this->_reset(); // 每次執行 sql 後將各構造字符串恢復初始狀態,保證下一次查詢的正確性 } catch (PDOException $e) { throw $e; } }
上述的 get() 方法是直接取得整個結果集。而有一些業務邏輯但願只取一行結果,那麼就須要一個 row() 方法來實現這個需求了。
row() 方法並不難,只需把 get() 方法中的 PDOStatement::fetchAll() 方法改成 PDOStatement::fetch() 方法便可:
public function row() { $this->_buildQuery(); $this->_execute(); return $this->_pdoSt->fetch(PDO::FETCH_ASSOC); }
這裏就很少說了,你們能夠本身測試一下結果。
對於典型 web 環境而言,一次 sql 的查詢已經隨着 HTTP 的請求而結束,PHP 的垃圾回收功能會回收一次請求週期內的數據。而一次 HTTP 請求的時間也相對較短,基本不用考慮數據庫斷線的問題。
但在常駐內存的環境下,尤爲是單例模式下,數據庫驅動類可能一直在內存中不被銷燬。若是很長時間內沒有對數據庫進行訪問的話,由數據庫驅動類創建的數據庫鏈接會被數據庫做爲空閒鏈接切斷 (具體時間由數據庫設置決定),此時若是依舊使用舊的鏈接對象,會出現持續報錯的問題。也就是說,咱們要對數據庫斷線的狀況進行處理,在檢測到斷線的同時新建一個鏈接代替舊的鏈接繼續使用。【1】
在 PDO 中,數據庫斷線後繼續訪問會相應的拋出一個 PDOException 異常 (也能夠是一個錯誤,由 PDO 的錯誤處理設置決定)。
當數據庫出現錯誤時,PDOException 實例的 errorInfo 屬性中保存了錯誤的詳細信息數組,第一個元素返回 SQLSTATE error code,第二個元素是具體驅動錯誤碼,第三個元素是具體的錯誤信息。參見 PDO::errorInfo
Mysql 斷線相關的錯誤碼有兩個:
PostgreSql 斷線相關的錯誤碼有一個:
當具體驅動錯誤碼爲 7 時 PostgreSql 斷線 (此驅動錯誤碼根據 PDOException 實測得出,暫時未找到相關文檔)
Sqlite 基於內存和文件,不存在斷線一說,不作考慮。
這裏咱們使用 PDO 的具體驅動錯誤碼做爲判斷斷線的依據。
基類添加 _isTimeout() 方法:
protected function _isTimeout(PDOException $e) { // 異常信息知足斷線條件,則返回 true return ( $e->errorInfo[1] == 2006 || // MySQL server has gone away (CR_SERVER_GONE_ERROR) $e->errorInfo[1] == 2013 || // Lost connection to MySQL server during query (CR_SERVER_LOST) $e->errorInfo[1] == 7 // no connection to the server (for postgresql) ); }
修改 _execute() 方法,添加斷線重連功能:
protected function _execute() { try { $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_pdoSt->execute(); $this->_reset(); } catch (PDOException $e) { // PDO 拋出異常,判斷是不是數據庫斷線引發 if($this->_isTimeout($e)) { // 斷線異常,清除舊的數據庫鏈接,從新鏈接 $this->_closeConnection(); $this->_connect(); // 重試異常前的操做 try { $this->_pdoSt = $this->_pdo->prepare($this->_prepare_sql); $this->_pdoSt->execute(); $this->_reset(); } catch (PDOException $e) { // 仍是失敗、向外拋出異常 throw $e; } } else { // 非斷線引發的異常,向外拋出,交給外部邏輯處理 throw $e; } } }
順便把以前暴露的 PDO 的原生接口也支持斷線重連:
public function query($sql) { try { return $this->_pdo->query($sql); } catch (PDOException $e) { // when time out, reconnect if($this->_isTimeout($e)) { $this->_closeConnection(); $this->_connect(); try { return $this->_pdo->query($sql); } catch (PDOException $e) { throw $e; } } else { throw $e; } } } public function exec($sql) { try { return $this->_pdo->exec($sql); } catch (PDOException $e) { // when time out, reconnect if($this->_isTimeout($e)) { $this->_closeConnection(); $this->_connect(); try { return $this->_pdo->exec($sql); } catch (PDOException $e) { throw $e; } } else { throw $e; } } } public function prepare($sql, array $driver_options = []) { try { return $this->_pdo->prepare($sql, $driver_options); } catch (PDOException $e) { // when time out, reconnect if($this->_isTimeout($e)) { $this->_closeConnection(); $this->_connect(); try { return $this->_pdo->prepare($sql, $driver_options); } catch (PDOException $e) { throw $e; } } else { throw $e; } } }
如何模擬斷線?
在內存常駐模式中 (如 workerman 的 server 監聽環境下):
Just do it