代碼裡原有的註釋已經非常完善瞭。不需要我在做什麼瞭。。。。。
namespace think\db; use PDO; use PDOStatement; use think\Collection; use think\Db; use think\db\exception\BindParamException; use think\db\Query; use think\Debug; use think\Exception; use think\exception\PDOException; use think\Log; /** 伺服器 連接 抽象類 * Class Connection * @package think * @method Query table(string $table) 指定數據表(含前綴) * @method Query name(string $name) 指定數據表(不含前綴) * */ abstract class Connection { /** @var PDOStatement PDO操作實例 */ protected $PDOStatement; /** @var string 當前SQL指令 */ protected $queryStr = ''; // 返回或者影響記錄數 protected $numRows = 0; // 事務指令數 protected $transTimes = 0; // 錯誤信息 protected $error = ''; /** @var PDO[] 伺服器連接ID 支持多個連接 */ protected $links = []; /** @var PDO 當前連接ID */ protected $linkID; protected $linkRead; protected $linkWrite; // 查詢結果類型 protected $resultSetType = 'array'; // 查詢結果類型 protected $fetchType = PDO::FETCH_ASSOC; // 字段屬性大小寫 protected $attrCase = PDO::CASE_LOWER; // 監聽回調 protected static $event = []; // 查詢對象 protected $query = []; // 伺服器連接參數配置 protected $config = [ // 伺服器類型 'type' => '', // 伺服器地址 'hostname' => '', // 伺服器名 'database' => '', // 用戶名 'username' => '', // 密碼 'password' => '', // 端口 'hostport' => '', // 連接dsn 'dsn' => '', // 伺服器連接參數 'params' => [], // 伺服器編碼默認采用utf8 'charset' => 'utf8', // 伺服器表前綴 'prefix' => '', // 伺服器調試模式 'debug' => false, // 伺服器部署方式:0 集中式(單一伺服器),1 分佈式(主從伺服器) 'deploy' => 0, // 伺服器讀寫是否分離 主從式有效 'rw_separate' => false, // 讀寫分離後 主伺服器數量 'master_num' => 1, // 指定從伺服器序號 'slave_no' => '', // 是否嚴格檢查字段是否存在 'fields_strict' => true, // 數據集返回類型 'resultset_type' => 'array', // 自動寫入時間戳字段 'auto_timestamp' => false, // 是否需要進行SQL性能分析 'sql_explain' => false, // Builder類 'builder' => '', // Query類 'query' => '\\think\\db\\Query', ]; // PDO連接參數 protected $params = [ PDO::ATTR_CASE => PDO::CASE_NATURAL, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, PDO::ATTR_STRINGIFY_FETCHES => false, PDO::ATTR_EMULATE_PREPARES => false, ]; /** * 架構函數 讀取伺服器配置信息 * @access public * @param array $config 伺服器配置數組 */ public function __construct(array $config = []) { if (!empty($config)) { $this->config = array_merge($this->config, $config); } } /** * 創建指定模型的查詢對象 * @access public * @param string $model 模型類名稱 * @param string $queryClass 查詢對象類名 * @return Query */ public function model($model, $queryClass = '') { // 查詢對象 query 中 $model 不存在時 執行 if (!isset($this->query[$model])) { // 查詢類 命名空間地址 $class = $queryClass ?: $this->config['query']; //實例化該類 並賦值 //$this 當前對象,$model 模型類名稱 $this->query[$model] = new $class($this, $model); } return $this->query[$model]; } /** * 調用Query類的查詢方法 * @access public * @param string $method 方法名稱 * @param array $args 調用參數 * @return mixed */ public function __call($method, $args) { if (!isset($this->query['database'])) { // 查詢類 命名空間地址 $class = $this->config['query']; // 實例化並賦值 //$this 當前對象 $this->query['database'] = new $class($this); } return call_user_func_array([$this->query['database'], $method], $args); } /** * 解析pdo連接的dsn信息 * @access protected * @param array $config 連接信息 * @return string */ abstract protected function parseDsn($config); /** * 取得數據表的字段信息 * @access public * @param string $tableName * @return array */ abstract public function getFields($tableName); /** * 取得伺服器的表信息 * @access public * @param string $dbName * @return array */ abstract public function getTables($dbName); /** * SQL性能分析 * @access protected * @param string $sql * @return array */ abstract protected function getExplain($sql); /** * 對返數據表字段信息進行大小寫轉換出來 * @access public * @param array $info 字段信息 * @return array */ public function fieldCase($info) { // 字段大小寫轉換 switch ($this->attrCase) { case PDO::CASE_LOWER: $info = array_change_key_case($info); break; case PDO::CASE_UPPER: $info = array_change_key_case($info, CASE_UPPER); break; case PDO::CASE_NATURAL: default: // 不做轉換 } return $info; } /** * 獲取伺服器的配置參數 * @access public * @param string $config 配置名稱 * @return mixed */ public function getConfig($config = '') { return $config ? $this->config[$config] : $this->config; } /** * 設置伺服器的配置參數 * @access public * @param string|array $config 配置名稱 * @param mixed $value 配置值 * @return void */ public function setConfig($config, $value = '') { if (is_array($config)) { $this->config = array_merge($this->config, $config); } else { $this->config[$config] = $value; } } /** * 連接伺服器方法 * @access public * @param array $config 連接參數 * @param integer $linkNum 連接序號 * @param array|bool $autoConnection 是否自動連接主伺服器(用於分佈式) * @return PDO * @throws Exception */ public function connect(array $config = [], $linkNum = 0, $autoConnection = false) { // 伺服器鏈接links中序號為$linkNum是否已存在,如果不存在時執行 if (!isset($this->links[$linkNum])) { // $config 連接參數不存在時,自動賦值 if (!$config) { $config = $this->config; } else { // 合並鏈接參數 $config = array_merge($this->config, $config); } // 連接參數 if (isset($config['params']) && is_array($config['params'])) { $params = $config['params'] + $this->params; } else { $params = $this->params; } // 記錄當前字段屬性大小寫設置 $this->attrCase = $params[PDO::ATTR_CASE]; // 記錄數據集返回類型 if (isset($config['resultset_type'])) { $this->resultSetType = $config['resultset_type']; } try { // $config 連接參數 dsn 不存在時 if (empty($config['dsn'])) { // 解析$config變量,生成 dsn 格式 $config['dsn'] = $this->parseDsn($config); } if ($config['debug']) { $startTime = microtime(true); } // PDO 鏈接實例化 $this->links[$linkNum] = new PDO($config['dsn'], $config['username'], $config['password'], $params); // 伺服器配置 是否開啟 debug 模式 if ($config['debug']) { // 記錄伺服器連接信息 Log::record('[ DB ] CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn'], 'sql'); } } catch (\PDOException $e) { // 分佈式伺服器 是否開啟 if ($autoConnection) { Log::record($e->getMessage(), 'error'); // 重新鏈接 return $this->connect($autoConnection, $linkNum); } else { throw $e; } } } return $this->links[$linkNum]; } /** * 釋放查詢結果 * @access public */ public function free() { $this->PDOStatement = null; } /** * 獲取PDO對象 * @access public * @return \PDO|false */ public function getPdo() { if (!$this->linkID) { return false; } else { return $this->linkID; } } /** * 執行查詢 返回數據集 * @access public * @param string $sql sql指令 * @param array $bind 參數綁定 * @param boolean $master 是否在主伺服器讀操作 * @param bool|string $class 指定返回的數據集對象 * @return mixed * @throws BindParamException * @throws PDOException */ public function query($sql, $bind = [], $master = false, $class = false) { $this->initConnect($master); if (!$this->linkID) { return false; } // 根據參數綁定組裝最終的SQL語句 $this->queryStr = $this->getRealSql($sql, $bind); //釋放前次的查詢結果 if (!empty($this->PDOStatement)) { $this->free(); } Db::$queryTimes++; try { // 調試開始 $this->debug(true); // 預處理 $this->PDOStatement = $this->linkID->prepare($sql); // 參數綁定 $this->bindValue($bind); // 執行查詢 $result = $this->PDOStatement->execute(); // 調試結束 $this->debug(false); $procedure = in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']); return $this->getResult($class, $procedure); } catch (\PDOException $e) { throw new PDOException($e, $this->config, $this->queryStr); } } /** * 執行語句 * @access public * @param string $sql sql指令 * @param array $bind 參數綁定 * @return int * @throws BindParamException * @throws PDOException */ public function execute($sql, $bind = []) { $this->initConnect(true); if (!$this->linkID) { return false; } // 根據參數綁定組裝最終的SQL語句 $this->queryStr = $this->getRealSql($sql, $bind); //釋放前次的查詢結果 if (!empty($this->PDOStatement)) { $this->free(); } Db::$executeTimes++; try { // 調試開始 $this->debug(true); // 預處理 $this->PDOStatement = $this->linkID->prepare($sql); // 參數綁定操作 $this->bindValue($bind); // 執行語句 $result = $this->PDOStatement->execute(); // 調試結束 $this->debug(false); $this->numRows = $this->PDOStatement->rowCount(); return $this->numRows; } catch (\PDOException $e) { throw new PDOException($e, $this->config, $this->queryStr); } } /** * 根據參數綁定組裝最終的SQL語句 便於調試 * @access public * @param string $sql 帶參數綁定的sql語句 * @param array $bind 參數綁定列表 * @return string */ public function getRealSql($sql, array $bind = []) { if ($bind) { foreach ($bind as $key => $val) { $value = is_array($val) ? $val[0] : $val; $type = is_array($val) ? $val[1] : PDO::PARAM_STR; if (PDO::PARAM_STR == $type) { $value = $this->quote($value); } // 判斷占位符 $sql = is_numeric($key) ? substr_replace($sql, $value, strpos($sql, '?'), 1) : str_replace( [':' . $key . ')', ':' . $key . ',', ':' . $key . ' '], [$value . ')', $value . ',', $value . ' '], $sql . ' '); } } return $sql; } /** * 參數綁定 * 支持 ['name'=>'value','id'=>123] 對應命名占位符 * 或者 ['value',123] 對應問號占位符 * @access public * @param array $bind 要綁定的參數列表 * @return void * @throws \think\Exception */ protected function bindValue(array $bind = []) { foreach ($bind as $key => $val) { // 占位符 $param = is_numeric($key) ? $key + 1 : ':' . $key; if (is_array($val)) { $result = $this->PDOStatement->bindValue($param, $val[0], $val[1]); } else { $result = $this->PDOStatement->bindValue($param, $val); } if (!$result) { throw new BindParamException( "Error occurred when binding parameters '{$param}'", $this->config, $this->queryStr, $bind ); } } } /** * 獲得數據集 * @access protected * @param bool|string $class true 返回PDOStatement 字符串用於指定返回的類名 * @param bool $procedure 是否存儲過程 * @return mixed */ protected function getResult($class = '', $procedure = false) { if (true === $class) { // 返回PDOStatement對象處理 return $this->PDOStatement; } if ($procedure) { // 存儲過程返回結果 return $this->procedure($class); } $result = $this->PDOStatement->fetchAll($this->fetchType); $this->numRows = count($result); if (!empty($class)) { // 返回指定數據集對象類 $result = new $class($result); } elseif ('collection' == $this->resultSetType) { // 返回數據集Collection對象 $result = new Collection($result); } return $result; } /** * 獲得存儲過程數據集 * @access protected * @param bool|string $class true 返回PDOStatement 字符串用於指定返回的類名 * @return array */ protected function procedure($class) { $item = []; do { $result = $this->getResult($class); if ($result) { $item[] = $result; } } while ($this->PDOStatement->nextRowset()); $this->numRows = count($item); return $item; } /** * 執行伺服器事務 * @access public * @param callable $callback 數據操作方法回調 * @return mixed * @throws PDOException * @throws \Exception * @throws \Throwable */ public function transaction($callback) { $this->startTrans(); try { $result = null; if (is_callable($callback)) { $result = call_user_func_array($callback, [$this]); } $this->commit(); return $result; } catch (\Exception $e) { $this->rollback(); throw $e; } catch (\Throwable $e) { $this->rollback(); throw $e; } } /** * 啟動事務 * @access public * @return void */ public function startTrans() { // 初始化伺服器連接 $this->initConnect(true); if (!$this->linkID) { return false; } ++$this->transTimes; if (1 == $this->transTimes) { $this->linkID->beginTransaction(); } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { $this->linkID->exec( $this->parseSavepoint('trans' . $this->transTimes) ); } } /** * 用於非自動提交狀態下面的查詢提交 * @access public * @return void * @throws PDOException */ public function commit() { $this->initConnect(true); if (1 == $this->transTimes) { $this->linkID->commit(); } --$this->transTimes; } /** * 事務回滾 * @access public * @return void * @throws PDOException */ public function rollback() { $this->initConnect(true); if (1 == $this->transTimes) { $this->linkID->rollBack(); } elseif ($this->transTimes > 1 && $this->supportSavepoint()) { $this->linkID->exec( $this->parseSavepointRollBack('trans' . $this->transTimes) ); } $this->transTimes = max(0, $this->transTimes - 1); } /** * 是否支持事務嵌套 * @return bool */ protected function supportSavepoint() { return false; } /** * 生成定義保存點的SQL * @param $name * @return string */ protected function parseSavepoint($name) { return 'SAVEPOINT ' . $name; } /** * 生成回滾到保存點的SQL * @param $name * @return string */ protected function parseSavepointRollBack($name) { return 'ROLLBACK TO SAVEPOINT ' . $name; } /** * 批處理執行SQL語句 * 批處理的指令都認為是execute操作 * @access public * @param array $sqlArray SQL批處理指令 * @return boolean */ public function batchQuery($sqlArray = []) { if (!is_array($sqlArray)) { return false; } // 自動啟動事務支持 $this->startTrans(); try { foreach ($sqlArray as $sql) { $this->execute($sql); } // 提交事務 $this->commit(); } catch (\Exception $e) { $this->rollback(); throw $e; } return true; } /** * 獲得查詢次數 * @access public * @param boolean $execute 是否包含所有查詢 * @return integer */ public function getQueryTimes($execute = false) { return $execute ? Db::$queryTimes + Db::$executeTimes : Db::$queryTimes; } /** * 獲得執行次數 * @access public * @return integer */ public function getExecuteTimes() { return Db::$executeTimes; } /** * 關閉伺服器 * @access public */ public function close() { $this->linkID = null; } /** * 獲取最近一次查詢的sql語句 * @access public * @return string */ public function getLastSql() { return $this->queryStr; } /** * 獲取最近插入的ID * @access public * @param string $sequence 自增序列名 * @return string */ public function getLastInsID($sequence = null) { return $this->linkID->lastInsertId($sequence); } /** * 獲取返回或者影響的記錄數 * @access public * @return integer */ public function getNumRows() { return $this->numRows; } /** * 獲取最近的錯誤信息 * @access public * @return string */ public function getError() { if ($this->PDOStatement) { $error = $this->PDOStatement->errorInfo(); $error = $error[1] . ':' . $error[2]; } else { $error = ''; } if ('' != $this->queryStr) { $error .= "\n [ SQL語句 ] : " . $this->queryStr; } return $error; } /** * SQL指令安全過濾 * @access public * @param string $str SQL字符串 * @param bool $master 是否主庫查詢 * @return string */ public function quote($str, $master = true) { $this->initConnect($master); return $this->linkID ? $this->linkID->quote($str) : $str; } /** * 伺服器調試 記錄當前SQL及分析性能 * @access protected * @param boolean $start 調試開始標記 true 開始 false 結束 * @param string $sql 執行的SQL語句 留空自動獲取 * @return void */ protected function debug($start, $sql = '') { if (!empty($this->config['debug'])) { // 開啟伺服器調試模式 if ($start) { Debug::remark('queryStartTime', 'time'); } else { // 記錄操作結束時間 Debug::remark('queryEndTime', 'time'); $runtime = Debug::getRangeTime('queryStartTime', 'queryEndTime'); $sql = $sql ?: $this->queryStr; $log = $sql . ' [ RunTime:' . $runtime . 's ]'; $result = []; // SQL性能分析 if ($this->config['sql_explain'] && 0 === stripos(trim($sql), 'select')) { $result = $this->getExplain($sql); } // SQL監聽 $this->trigger($sql, $runtime, $result); } } } /** * 監聽SQL執行 * @access public * @param callable $callback 回調方法 * @return void */ public function listen($callback) { self::$event[] = $callback; } /** * 觸發SQL事件 * @access protected * @param string $sql SQL語句 * @param float $runtime SQL運行時間 * @param mixed $explain SQL分析 * @return bool */ protected function trigger($sql, $runtime, $explain = []) { if (!empty(self::$event)) { foreach (self::$event as $callback) { if (is_callable($callback)) { call_user_func_array($callback, [$sql, $runtime, $explain]); } } } else { // 未註冊監聽則記錄到日志中 Log::record('[ SQL ] ' . $sql . ' [ RunTime:' . $runtime . 's ]', 'sql'); if (!empty($explain)) { Log::record('[ EXPLAIN : ' . var_export($explain, true) . ' ]', 'sql'); } } } /** * 初始化伺服器連接 * @access protected * @param boolean $master 是否主伺服器 * @return void */ protected function initConnect($master = true) { if (!empty($this->config['deploy'])) { // 采用分佈式伺服器 if ($master) { if (!$this->linkWrite) { $this->linkWrite = $this->multiConnect(true); } $this->linkID = $this->linkWrite; } else { if (!$this->linkRead) { $this->linkRead = $this->multiConnect(false); } $this->linkID = $this->linkRead; } } elseif (!$this->linkID) { // 默認單伺服器 $this->linkID = $this->connect(); } } /** * 連接分佈式伺服器 * @access protected * @param boolean $master 主伺服器 * @return PDO */ protected function multiConnect($master = false) { $_config = []; // 分佈式伺服器配置解析 foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { $_config[$name] = explode(',', $this->config[$name]); } // 主伺服器序號 $m = floor(mt_rand(0, $this->config['master_num'] - 1)); if ($this->config['rw_separate']) { // 主從式采用讀寫分離 if ($master) // 主伺服器寫入 { $r = $m; } elseif (is_numeric($this->config['slave_no'])) { // 指定伺服器讀 $r = $this->config['slave_no']; } else { // 讀操作連接從伺服器 每次隨機連接的伺服器 $r = floor(mt_rand($this->config['master_num'], count($_config['hostname']) - 1)); } } else { // 讀寫操作不區分伺服器 每次隨機連接的伺服器 $r = floor(mt_rand(0, count($_config['hostname']) - 1)); } $dbMaster = false; if ($m != $r) { $dbMaster = []; foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { $dbMaster[$name] = isset($_config[$name][$m]) ? $_config[$name][$m] : $_config[$name][0]; } } $dbConfig = []; foreach (['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'] as $name) { $dbConfig[$name] = isset($_config[$name][$r]) ? $_config[$name][$r] : $_config[$name][0]; } return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster); } /** * 析構方法 * @access public */ public function __destruct() { // 釋放查詢 if ($this->PDOStatement) { $this->free(); } // 關閉連接 $this->close(); } }